diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js index e2d9ba76660f9..fbc9beb8503d8 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -1,5 +1,6 @@ let React; -let ReactNoop; +let ReactDOM; +let ReactDOMClient; let Scheduler; let act; let useState; @@ -15,7 +16,8 @@ describe('ReactFlushSync', () => { jest.resetModules(); React = require('react'); - ReactNoop = require('react-noop-renderer'); + ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; useState = React.useState; @@ -32,7 +34,49 @@ describe('ReactFlushSync', () => { return text; } - test('changes priority of updates in useEffect', async () => { + function getVisibleChildren(element: Element): React$Node { + const children = []; + let node: any = element.firstChild; + while (node) { + if (node.nodeType === 1) { + if ( + ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') || + node.hasAttribute('data-meaningful')) && + node.tagName !== 'TEMPLATE' && + node.tagName !== 'template' && + !node.hasAttribute('hidden') && + !node.hasAttribute('aria-hidden') + ) { + const props: any = {}; + const attributes = node.attributes; + for (let i = 0; i < attributes.length; i++) { + if ( + attributes[i].name === 'id' && + attributes[i].value.includes(':') + ) { + // We assume this is a React added ID that's a non-visual implementation detail. + continue; + } + props[attributes[i].name] = attributes[i].value; + } + props.children = getVisibleChildren(node); + children.push( + require('react').createElement(node.tagName.toLowerCase(), props), + ); + } + } else if (node.nodeType === 3) { + children.push(node.data); + } + node = node.nextSibling; + } + return children.length === 0 + ? undefined + : children.length === 1 + ? children[0] + : children; + } + + it('changes priority of updates in useEffect', async () => { spyOnDev(console, 'error').mockImplementation(() => {}); function App() { @@ -41,13 +85,14 @@ describe('ReactFlushSync', () => { useEffect(() => { if (syncState !== 1) { setState(1); - ReactNoop.flushSync(() => setSyncState(1)); + ReactDOM.flushSync(() => setSyncState(1)); } }, [syncState, state]); return ; } - const root = ReactNoop.createRoot(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); await act(async () => { React.startTransition(() => { root.render(); @@ -62,7 +107,7 @@ describe('ReactFlushSync', () => { ); // The remaining update is not sync - ReactNoop.flushSync(); + ReactDOM.flushSync(); assertLog([]); if (gate(flags => flags.enableUnifiedSyncLane)) { @@ -72,7 +117,7 @@ describe('ReactFlushSync', () => { await waitForPaint(['1, 1']); } }); - expect(root).toMatchRenderedOutput('1, 1'); + expect(getVisibleChildren(container)).toEqual('1, 1'); if (__DEV__) { expect(console.error.mock.calls[0][0]).toContain( @@ -83,7 +128,7 @@ describe('ReactFlushSync', () => { } }); - test('nested with startTransition', async () => { + it('supports nested flushSync with startTransition', async () => { let setSyncState; let setState; function App() { @@ -94,20 +139,21 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createRoot(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); assertLog(['0, 0']); - expect(root).toMatchRenderedOutput('0, 0'); + expect(getVisibleChildren(container)).toEqual('0, 0'); await act(() => { - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { startTransition(() => { // This should be async even though flushSync is on the stack, because // startTransition is closer. setState(1); - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { // This should be async even though startTransition is on the stack, // because flushSync is closer. setSyncState(1); @@ -116,14 +162,14 @@ describe('ReactFlushSync', () => { }); // Only the sync update should have flushed assertLog(['1, 0']); - expect(root).toMatchRenderedOutput('1, 0'); + expect(getVisibleChildren(container)).toEqual('1, 0'); }); // Now the async update has flushed, too. assertLog(['1, 1']); - expect(root).toMatchRenderedOutput('1, 1'); + expect(getVisibleChildren(container)).toEqual('1, 1'); }); - test('flushes passive effects synchronously when they are the result of a sync render', async () => { + it('flushes passive effects synchronously when they are the result of a sync render', async () => { function App() { useEffect(() => { Scheduler.log('Effect'); @@ -131,9 +177,10 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createRoot(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); await act(() => { - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); assertLog([ @@ -142,11 +189,12 @@ describe('ReactFlushSync', () => { // flushSync should flush it. 'Effect', ]); - expect(root).toMatchRenderedOutput('Child'); + expect(getVisibleChildren(container)).toEqual('Child'); }); }); - test('do not flush passive effects synchronously after render in legacy mode', async () => { + // @gate !disableLegacyMode + it('does not flush passive effects synchronously after render in legacy mode', async () => { function App() { useEffect(() => { Scheduler.log('Effect'); @@ -154,23 +202,24 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createLegacyRoot(); + const container = document.createElement('div'); await act(() => { - ReactNoop.flushSync(() => { - root.render(); + ReactDOM.flushSync(() => { + ReactDOM.render(, container); }); assertLog([ 'Child', // Because we're in legacy mode, we shouldn't have flushed the passive // effects yet. ]); - expect(root).toMatchRenderedOutput('Child'); + expect(getVisibleChildren(container)).toEqual('Child'); }); // Effect flushes after paint. assertLog(['Effect']); }); - test('flush pending passive effects before scope is called in legacy mode', async () => { + // @gate !disableLegacyMode + it('flushes pending passive effects before scope is called in legacy mode', async () => { let currentStep = 0; function App({step}) { @@ -181,30 +230,31 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createLegacyRoot(); + const container = document.createElement('div'); await act(() => { - ReactNoop.flushSync(() => { - root.render(); + ReactDOM.flushSync(() => { + ReactDOM.render(, container); }); assertLog([ 1, // Because we're in legacy mode, we shouldn't have flushed the passive // effects yet. ]); - expect(root).toMatchRenderedOutput('1'); + expect(getVisibleChildren(container)).toMatchRenderedOutput('1'); - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { // This should render step 2 because the passive effect has already // fired, before the scope function is called. - root.render(); + ReactDOM.render(, container); }); assertLog(['Effect: 1', 2]); - expect(root).toMatchRenderedOutput('2'); + expect(getVisibleChildren(container)).toMatchRenderedOutput('2'); }); assertLog(['Effect: 2']); }); - test("do not flush passive effects synchronously when they aren't the result of a sync render", async () => { + // @gate !disableLegacyMode + it("does not flush passive effects synchronously when they aren't the result of a sync render", async () => { function App() { useEffect(() => { Scheduler.log('Effect'); @@ -212,21 +262,21 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createRoot(); + const container = document.createElement('div'); await act(async () => { - root.render(); + ReactDOM.render(, container); await waitForPaint([ 'Child', // Because the passive effect was not the result of a sync update, it // should not flush before paint. ]); - expect(root).toMatchRenderedOutput('Child'); + expect(getVisibleChildren(container)).toMatchRenderedOutput('Child'); }); // Effect flushes after paint. assertLog(['Effect']); }); - test('does not flush pending passive effects', async () => { + it('does not flush pending passive effects', async () => { function App() { useEffect(() => { Scheduler.log('Effect'); @@ -234,14 +284,15 @@ describe('ReactFlushSync', () => { return ; } - const root = ReactNoop.createRoot(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); await act(async () => { root.render(); await waitForPaint(['Child']); - expect(root).toMatchRenderedOutput('Child'); + expect(getVisibleChildren(container)).toEqual('Child'); // Passive effects are pending. Calling flushSync should not affect them. - ReactNoop.flushSync(); + ReactDOM.flushSync(); // Effects still haven't fired. assertLog([]); }); @@ -249,14 +300,19 @@ describe('ReactFlushSync', () => { assertLog(['Effect']); }); - test('completely exhausts synchronous work queue even if something throws', async () => { + it('completely exhausts synchronous work queue even if something throws', async () => { function Throws({error}) { throw error; } - const root1 = ReactNoop.createRoot(); - const root2 = ReactNoop.createRoot(); - const root3 = ReactNoop.createRoot(); + const container1 = document.createElement('div'); + const root1 = ReactDOMClient.createRoot(container1); + + const container2 = document.createElement('div'); + const root2 = ReactDOMClient.createRoot(container2); + + const container3 = document.createElement('div'); + const root3 = ReactDOMClient.createRoot(container3); await act(async () => { root1.render(); @@ -270,7 +326,7 @@ describe('ReactFlushSync', () => { let error; try { - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { root1.render(); root2.render(); root3.render(); @@ -283,9 +339,9 @@ describe('ReactFlushSync', () => { // earlier updates errored. assertLog(['aww']); // Roots 1 and 2 were unmounted. - expect(root1).toMatchRenderedOutput(null); - expect(root2).toMatchRenderedOutput(null); - expect(root3).toMatchRenderedOutput('aww'); + expect(getVisibleChildren(container1)).toEqual(undefined); + expect(getVisibleChildren(container2)).toEqual(undefined); + expect(getVisibleChildren(container3)).toEqual('aww'); // Because there were multiple errors, React threw an AggregateError. // eslint-disable-next-line no-undef diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js index 8c1bc7475be36..a5da4dbcc261e 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js @@ -1,5 +1,6 @@ let React; -let ReactNoop; +let ReactDOM; +let ReactDOMClient; let Scheduler; let act; let assertLog; @@ -36,7 +37,8 @@ describe('ReactFlushSync (AggregateError not available)', () => { }; React = require('react'); - ReactNoop = require('react-noop-renderer'); + ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; @@ -49,14 +51,59 @@ describe('ReactFlushSync (AggregateError not available)', () => { return text; } + function getVisibleChildren(element: Element): React$Node { + const children = []; + let node: any = element.firstChild; + while (node) { + if (node.nodeType === 1) { + if ( + ((node.tagName !== 'SCRIPT' && node.tagName !== 'script') || + node.hasAttribute('data-meaningful')) && + node.tagName !== 'TEMPLATE' && + node.tagName !== 'template' && + !node.hasAttribute('hidden') && + !node.hasAttribute('aria-hidden') + ) { + const props: any = {}; + const attributes = node.attributes; + for (let i = 0; i < attributes.length; i++) { + if ( + attributes[i].name === 'id' && + attributes[i].value.includes(':') + ) { + // We assume this is a React added ID that's a non-visual implementation detail. + continue; + } + props[attributes[i].name] = attributes[i].value; + } + props.children = getVisibleChildren(node); + children.push( + require('react').createElement(node.tagName.toLowerCase(), props), + ); + } + } else if (node.nodeType === 3) { + children.push(node.data); + } + node = node.nextSibling; + } + return children.length === 0 + ? undefined + : children.length === 1 + ? children[0] + : children; + } + test('completely exhausts synchronous work queue even if something throws', async () => { function Throws({error}) { throw error; } - const root1 = ReactNoop.createRoot(); - const root2 = ReactNoop.createRoot(); - const root3 = ReactNoop.createRoot(); + const container1 = document.createElement('div'); + const root1 = ReactDOMClient.createRoot(container1); + const container2 = document.createElement('div'); + const root2 = ReactDOMClient.createRoot(container2); + const container3 = document.createElement('div'); + const root3 = ReactDOMClient.createRoot(container3); await act(async () => { root1.render(); @@ -72,7 +119,7 @@ describe('ReactFlushSync (AggregateError not available)', () => { overrideQueueMicrotask = true; let error; try { - ReactNoop.flushSync(() => { + ReactDOM.flushSync(() => { root1.render(); root2.render(); root3.render(); @@ -85,9 +132,9 @@ describe('ReactFlushSync (AggregateError not available)', () => { // earlier updates errored. assertLog(['aww']); // Roots 1 and 2 were unmounted. - expect(root1).toMatchRenderedOutput(null); - expect(root2).toMatchRenderedOutput(null); - expect(root3).toMatchRenderedOutput('aww'); + expect(getVisibleChildren(container1)).toEqual(undefined); + expect(getVisibleChildren(container2)).toEqual(undefined); + expect(getVisibleChildren(container3)).toEqual('aww'); // In modern environments, React would throw an AggregateError. Because // AggregateError is not available, React throws the first error, then