diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e5bb31b2b3787..9f8129db9e4ee 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,6 @@ contact_links: - name: 📃 Documentation Issue - url: https://github.com/reactjs/reactjs.org/issues/new + url: https://github.com/reactjs/react.dev/issues/new/choose about: This issue tracker is not for documentation issues. Please file documentation issues here. - name: 🤔 Questions and Help url: https://reactjs.org/community/support.html diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index 4a4c63eaad105..6c7f60c2e2f13 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -20,7 +20,7 @@ describe('unmocked scheduler', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - DOMAct = React.unstable_act; + DOMAct = React.act; TestRenderer = require('react-test-renderer'); TestAct = TestRenderer.act; }); @@ -61,7 +61,7 @@ describe('mocked scheduler', () => { require.requireActual('scheduler/unstable_mock') ); React = require('react'); - DOMAct = React.unstable_act; + DOMAct = React.act; TestRenderer = require('react-test-renderer'); TestAct = TestRenderer.act; }); diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index eb58f8d4d1fbe..a26a5e19b2f27 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -7747,6 +7747,33 @@ const testsTypescript = { } `, }, + { + code: normalizeIndent` + function App(props) { + React.useEffect(() => { + console.log(props.test); + }, [props.test] as const); + } + `, + }, + { + code: normalizeIndent` + function App(props) { + React.useEffect(() => { + console.log(props.test); + }, [props.test] as any); + } + `, + }, + { + code: normalizeIndent` + function App(props) { + React.useEffect((() => { + console.log(props.test); + }) as any, [props.test]); + } + `, + }, ], invalid: [ { diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index 62a451377f6e6..6958466a2ff5a 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -618,7 +618,12 @@ export default { const declaredDependencies = []; const externalDependencies = new Set(); - if (declaredDependenciesNode.type !== 'ArrayExpression') { + const isArrayExpression = + declaredDependenciesNode.type === 'ArrayExpression'; + const isTSAsArrayExpression = + declaredDependenciesNode.type === 'TSAsExpression' && + declaredDependenciesNode.expression.type === 'ArrayExpression'; + if (!isArrayExpression && !isTSAsArrayExpression) { // If the declared dependencies are not an array expression then we // can't verify that the user provided the correct dependencies. Tell // the user this in an error. @@ -631,7 +636,11 @@ export default { 'dependencies.', }); } else { - declaredDependenciesNode.elements.forEach(declaredDependencyNode => { + const arrayExpression = isTSAsArrayExpression + ? declaredDependenciesNode.expression + : declaredDependenciesNode; + + arrayExpression.elements.forEach(declaredDependencyNode => { // Skip elided elements. if (declaredDependencyNode === null) { return; @@ -1214,6 +1223,15 @@ export default { isEffect, ); return; // Handled + case 'TSAsExpression': + visitFunctionWithDependencies( + callback.expression, + declaredDependenciesNode, + reactiveHook, + reactiveHookName, + isEffect, + ); + return; // Handled case 'Identifier': if (!declaredDependenciesNode) { // No deps, no problems. diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index fc351132a3135..8e038bfc0f805 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1700,4 +1700,429 @@ describe('ReactFlight', () => { expect(errors).toEqual([]); }); + + // @gate enableServerComponentKeys + it('preserves state when keying a server component', async () => { + function StatefulClient({name}) { + const [state] = React.useState(name.toLowerCase()); + return state; + } + const Stateful = clientReference(StatefulClient); + + function Item({item}) { + return ( +
+ {item} + +
+ ); + } + + function Items({items}) { + return items.map(item => { + return ; + }); + } + + const transport = ReactNoopFlightServer.render( + , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
Aa
+
Bb
+
Cc
+ , + ); + + const transport2 = ReactNoopFlightServer.render( + , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport2)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
Bb
+
Aa
+
Dd
+
Cc
+ , + ); + }); + + // @gate enableServerComponentKeys + it('does not inherit keys of children inside a server component', async () => { + function StatefulClient({name, initial}) { + const [state] = React.useState(initial); + return state; + } + const Stateful = clientReference(StatefulClient); + + function Item({item, initial}) { + // This key is the key of the single item of this component. + // It's NOT part of the key of the list the parent component is + // in. + return ( +
+ {item} + +
+ ); + } + + function IndirectItem({item, initial}) { + // Even though we render two items with the same child key this key + // should not conflict, because the key belongs to the parent slot. + return ; + } + + // These items don't have their own keys because they're in a fixed set + const transport = ReactNoopFlightServer.render( + <> + + + + + , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
A1
+
B2
+
C5
+
C6
+ , + ); + + // This means that they shouldn't swap state when the properties update + const transport2 = ReactNoopFlightServer.render( + <> + + + + + , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport2)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
B3
+
A4
+
C5
+
C6
+ , + ); + }); + + // @gate enableServerComponentKeys + it('shares state between single return and array return in a parent', async () => { + function StatefulClient({name, initial}) { + const [state] = React.useState(initial); + return state; + } + const Stateful = clientReference(StatefulClient); + + function Item({item, initial}) { + // This key is the key of the single item of this component. + // It's NOT part of the key of the list the parent component is + // in. + return ( + + {item} + + + ); + } + + function Condition({condition}) { + if (condition) { + return ; + } + // The first item in the fragment is the same as the single item. + return ( + <> + + + + ); + } + + function ConditionPlain({condition}) { + if (condition) { + return ( + + C + + + ); + } + // The first item in the fragment is the same as the single item. + return ( + <> + + C + + + + D + + + + ); + } + + const transport = ReactNoopFlightServer.render( + // This two item wrapper ensures we're already one step inside an array. + // A single item is not the same as a set when it's nested one level. + <> +
+ +
+
+ +
+
+ +
+ , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> +
+ A1 +
+
+ C1 +
+
+ C1 +
+ , + ); + + const transport2 = ReactNoopFlightServer.render( + <> +
+ +
+
+ +
+ {null} +
+ +
+ , + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport2)); + }); + + // We're intentionally breaking from the semantics here for efficiency of the protocol. + // In the case a Server Component inside a fragment is itself implicitly keyed but its + // return value has a key, then we need a wrapper fragment. This means they can't + // reconcile. To solve this we would need to add a wrapper fragment to every Server + // Component just in case it returns a fragment later which is a lot. + expect(ReactNoop).toMatchRenderedOutput( + <> +
+ A2{/* This should be A1 ideally */} + B3 +
+
+ C1 + D3 +
+
+ C1 + D3 +
+ , + ); + }); + + it('shares state between single return and array return in a set', async () => { + function StatefulClient({name, initial}) { + const [state] = React.useState(initial); + return state; + } + const Stateful = clientReference(StatefulClient); + + function Item({item, initial}) { + // This key is the key of the single item of this component. + // It's NOT part of the key of the list the parent component is + // in. + return ( + + {item} + + + ); + } + + function Condition({condition}) { + if (condition) { + return ; + } + // The first item in the fragment is the same as the single item. + return ( + <> + + + + ); + } + + function ConditionPlain({condition}) { + if (condition) { + return ( + + C + + + ); + } + // The first item in the fragment is the same as the single item. + return ( + <> + + C + + + + D + + + + ); + } + + const transport = ReactNoopFlightServer.render( + // This two item wrapper ensures we're already one step inside an array. + // A single item is not the same as a set when it's nested one level. +
+ + + +
, + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( +
+ A1 + C1 + C1 +
, + ); + + const transport2 = ReactNoopFlightServer.render( +
+ + + {null} + +
, + ); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport2)); + }); + + // We're intentionally breaking from the semantics here for efficiency of the protocol. + // The issue with this test scenario is that when the Server Component is in a set, + // the next slot can't be conditionally a fragment or single. That would require wrapping + // in an additional fragment for every single child just in case it every expands to a + // fragment. + expect(ReactNoop).toMatchRenderedOutput( +
+ A2{/* Should be A1 */} + B3 + C2{/* Should be C1 */} + D3 + C2{/* Should be C1 */} + D3 +
, + ); + }); + + // @gate enableServerComponentKeys + it('preserves state with keys split across async work', async () => { + let resolve; + const promise = new Promise(r => (resolve = r)); + + function StatefulClient({name}) { + const [state] = React.useState(name.toLowerCase()); + return state; + } + const Stateful = clientReference(StatefulClient); + + function Item({name}) { + if (name === 'A') { + return promise.then(() => ( +
+ {name} + +
+ )); + } + return ( +
+ {name} + +
+ ); + } + + const transport = ReactNoopFlightServer.render([ + , + null, + ]); + + // Create a gap in the stream + await resolve(); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput(
Aa
); + + const transport2 = ReactNoopFlightServer.render([ + null, + , + ]); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport2)); + }); + + expect(ReactNoop).toMatchRenderedOutput(
Ba
); + }); }); diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 111041548259b..b4b0f97e2edaa 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -8,6 +8,7 @@ */ import type { + Awaited, ReactContext, ReactProviderType, StartTransitionOptions, @@ -80,6 +81,14 @@ function getPrimitiveStackCache(): Map> { // This type check is for Flow only. Dispatcher.useMemoCache(0); } + if (typeof Dispatcher.useOptimistic === 'function') { + // This type check is for Flow only. + Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); + } + if (typeof Dispatcher.useFormState === 'function') { + // This type check is for Flow only. + Dispatcher.useFormState((s: mixed, p: mixed) => s, null); + } } finally { readHookLog = hookLog; hookLog = []; @@ -348,6 +357,46 @@ function useMemoCache(size: number): Array { return data; } +function useOptimistic( + passthrough: S, + reducer: ?(S, A) => S, +): [S, (A) => void] { + const hook = nextHook(); + let state; + if (hook !== null) { + state = hook.memoizedState; + } else { + state = passthrough; + } + hookLog.push({ + primitive: 'Optimistic', + stackError: new Error(), + value: state, + }); + return [state, (action: A) => {}]; +} + +function useFormState( + action: (Awaited, P) => S, + initialState: Awaited, + permalink?: string, +): [Awaited, (P) => void] { + const hook = nextHook(); // FormState + nextHook(); // ActionQueue + let state; + if (hook !== null) { + state = hook.memoizedState; + } else { + state = initialState; + } + hookLog.push({ + primitive: 'FormState', + stackError: new Error(), + value: state, + }); + return [state, (payload: P) => {}]; +} + const Dispatcher: DispatcherType = { use, readContext, @@ -361,6 +410,7 @@ const Dispatcher: DispatcherType = { useInsertionEffect, useMemo, useMemoCache, + useOptimistic, useReducer, useRef, useState, @@ -368,6 +418,7 @@ const Dispatcher: DispatcherType = { useSyncExternalStore, useDeferredValue, useId, + useFormState, }; // create a proxy to throw a custom error diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index e220939a81e8b..ac4cb2e4a2b0f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -11,6 +11,7 @@ 'use strict'; let React; +let ReactDOM; let ReactTestRenderer; let ReactDebugTools; let act; @@ -21,6 +22,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); + ReactDOM = require('react-dom'); act = require('internal-test-utils').act; ReactDebugTools = require('react-debug-tools'); useMemoCache = React.unstable_useMemoCache; @@ -1106,4 +1108,82 @@ describe('ReactHooksInspectionIntegration', () => { }, ]); }); + + // @gate enableAsyncActions + it('should support useOptimistic hook', () => { + const useOptimistic = React.useOptimistic; + function Foo() { + const [value] = useOptimistic('abc', currentState => currentState); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + return value; + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root.findByType(Foo)._currentFiber(); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'Optimistic', + value: 'abc', + subHooks: [], + }, + { + id: 1, + isStateEditable: false, + name: 'Memo', + value: 'memo', + subHooks: [], + }, + { + id: 2, + isStateEditable: false, + name: 'Memo', + value: 'not used', + subHooks: [], + }, + ]); + }); + + // @gate enableFormActions && enableAsyncActions + it('should support useFormState hook', () => { + function Foo() { + const [value] = ReactDOM.useFormState(function increment(n) { + return n; + }, 0); + React.useMemo(() => 'memo', []); + React.useMemo(() => 'not used', []); + + return value; + } + + const renderer = ReactTestRenderer.create(); + const childFiber = renderer.root.findByType(Foo)._currentFiber(); + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'FormState', + value: 0, + subHooks: [], + }, + { + id: 1, + isStateEditable: false, + name: 'Memo', + value: 'memo', + subHooks: [], + }, + { + id: 2, + isStateEditable: false, + name: 'Memo', + value: 'not used', + subHooks: [], + }, + ]); + }); }); diff --git a/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js index f95d1a2d5fda7..2ef5ae8d02393 100644 --- a/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js +++ b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js @@ -7,25 +7,20 @@ * @flow */ +import {getVersionedRenderImplementation} from './utils'; + describe('Fast Refresh', () => { let React; let ReactFreshRuntime; let act; let babel; - let container; let exportsObj; let freshPlugin; - let legacyRender; let store; let withErrorsOrWarningsIgnored; - afterEach(() => { - jest.resetModules(); - }); - beforeEach(() => { exportsObj = undefined; - container = document.createElement('div'); babel = require('@babel/core'); freshPlugin = require('react-refresh/babel'); @@ -39,10 +34,12 @@ describe('Fast Refresh', () => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; }); + const {render: renderImplementation, getContainer} = + getVersionedRenderImplementation(); + function execute(source) { const compiled = babel.transform(source, { babelrc: false, @@ -73,7 +70,7 @@ describe('Fast Refresh', () => { function render(source) { const Component = execute(source); act(() => { - legacyRender(, container); + renderImplementation(); }); // Module initialization shouldn't be counted as a hot update. expect(ReactFreshRuntime.performReactRefresh()).toBe(null); @@ -98,7 +95,7 @@ describe('Fast Refresh', () => { // Here, we'll just force a re-render using the newer type to emulate this. const NextComponent = nextExports.default; act(() => { - legacyRender(, container); + renderImplementation(); }); } act(() => { @@ -142,8 +139,8 @@ describe('Fast Refresh', () => { `); - let element = container.firstChild; - expect(container.firstChild).not.toBe(null); + let element = getContainer().firstChild; + expect(getContainer().firstChild).not.toBe(null); patch(` function Parent() { @@ -163,8 +160,8 @@ describe('Fast Refresh', () => { `); // State is preserved; this verifies that Fast Refresh is wired up. - expect(container.firstChild).toBe(element); - element = container.firstChild; + expect(getContainer().firstChild).toBe(element); + element = getContainer().firstChild; patch(` function Parent() { @@ -184,7 +181,7 @@ describe('Fast Refresh', () => { `); // State is reset because hooks changed. - expect(container.firstChild).not.toBe(element); + expect(getContainer().firstChild).not.toBe(element); }); // @reactVersion >= 16.9 diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js index 401af1d9c5546..3f3ce3774332e 100644 --- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js +++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js @@ -7,12 +7,11 @@ * @flow */ -import {normalizeCodeLocInfo} from './utils'; +import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils'; describe('component stack', () => { let React; let act; - let legacyRender; let mockError; let mockWarn; @@ -30,11 +29,12 @@ describe('component stack', () => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; React = require('react'); }); + const {render} = getVersionedRenderImplementation(); + // @reactVersion >=16.9 it('should log the current component stack along with an error or warning', () => { const Grandparent = () => ; @@ -45,9 +45,7 @@ describe('component stack', () => { return null; }; - const container = document.createElement('div'); - - act(() => legacyRender(, container)); + act(() => render()); expect(mockError).toHaveBeenCalledWith( 'Test error.', @@ -79,8 +77,7 @@ describe('component stack', () => { return null; }; - const container = document.createElement('div'); - act(() => legacyRender(, container)); + act(() => render()); expect(useEffectCount).toBe(1); diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index e70c8e3a0afa8..e2674b10f3526 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -7,13 +7,12 @@ * @flow */ -import {normalizeCodeLocInfo} from './utils'; +import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils'; let React; let ReactDOMClient; let act; let fakeConsole; -let legacyRender; let mockError; let mockInfo; let mockGroup; @@ -67,9 +66,10 @@ describe('console', () => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; }); + const {render} = getVersionedRenderImplementation(); + // @reactVersion >=18.0 it('should not patch console methods that are not explicitly overridden', () => { expect(fakeConsole.error).not.toBe(mockError); @@ -185,7 +185,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockWarn).toHaveBeenCalledTimes(1); expect(mockWarn.mock.calls[0]).toHaveLength(1); @@ -215,7 +215,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog.mock.calls[0]).toHaveLength(1); @@ -256,7 +256,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog.mock.calls[0]).toHaveLength(1); @@ -313,9 +313,8 @@ describe('console', () => { } } - const container = document.createElement('div'); - act(() => legacyRender(, container)); - act(() => legacyRender(, container)); + act(() => render()); + act(() => render()); expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog.mock.calls[0]).toHaveLength(1); @@ -367,7 +366,7 @@ describe('console', () => { } } - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog.mock.calls[0]).toHaveLength(1); @@ -396,7 +395,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockWarn).toHaveBeenCalledTimes(1); expect(mockWarn.mock.calls[0]).toHaveLength(1); @@ -410,7 +409,7 @@ describe('console', () => { breakOnConsoleErrors: false, showInlineWarningsAndErrors: false, }); - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockWarn).toHaveBeenCalledTimes(2); expect(mockWarn.mock.calls[1]).toHaveLength(2); @@ -457,7 +456,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog.mock.calls[0]).toHaveLength(1); @@ -483,7 +482,7 @@ describe('console', () => { return null; }; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(mockWarn).toHaveBeenCalledTimes(1); expect(mockWarn.mock.calls[0][0]).toBe('Symbol:'); @@ -824,7 +823,6 @@ describe('console error', () => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; }); // @reactVersion >=18.0 diff --git a/packages/react-devtools-shared/src/__tests__/editing-test.js b/packages/react-devtools-shared/src/__tests__/editing-test.js index 281a2eb1836ed..cece83ba708aa 100644 --- a/packages/react-devtools-shared/src/__tests__/editing-test.js +++ b/packages/react-devtools-shared/src/__tests__/editing-test.js @@ -10,11 +10,12 @@ import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type Store from 'react-devtools-shared/src/devtools/store'; +import {getVersionedRenderImplementation} from './utils'; + describe('editing interface', () => { let PropTypes; let React; let bridge: FrontendBridge; - let legacyRender; let store: Store; let utils; @@ -25,8 +26,6 @@ describe('editing interface', () => { beforeEach(() => { utils = require('./utils'); - legacyRender = utils.legacyRender; - bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; @@ -36,6 +35,8 @@ describe('editing interface', () => { React = require('react'); }); + const {render} = getVersionedRenderImplementation(); + describe('props', () => { let committedClassProps; let committedFunctionProps; @@ -66,9 +67,8 @@ describe('editing interface', () => { inputRef = React.createRef(null); - const container = document.createElement('div'); await utils.actAsync(() => - legacyRender( + render( <> { , , - container, ), ); @@ -440,11 +439,9 @@ describe('editing interface', () => { } } - const container = document.createElement('div'); await utils.actAsync(() => - legacyRender( + render( , - container, ), ); @@ -662,10 +659,7 @@ describe('editing interface', () => { return null; } - const container = document.createElement('div'); - await utils.actAsync(() => - legacyRender(, container), - ); + await utils.actAsync(() => render()); hookID = 0; // index id = ((store.getElementIDAtIndex(0): any): number); @@ -917,13 +911,11 @@ describe('editing interface', () => { } } - const container = document.createElement('div'); await utils.actAsync(() => - legacyRender( + render( , - container, ), ); diff --git a/packages/react-devtools-shared/src/__tests__/ownersListContext-test.js b/packages/react-devtools-shared/src/__tests__/ownersListContext-test.js index c02a895feeaec..599492bed54a1 100644 --- a/packages/react-devtools-shared/src/__tests__/ownersListContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/ownersListContext-test.js @@ -12,11 +12,12 @@ import type {Element} from 'react-devtools-shared/src/frontend/types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type Store from 'react-devtools-shared/src/devtools/store'; +import {getVersionedRenderImplementation} from './utils'; + describe('OwnersListContext', () => { let React; let TestRenderer: ReactTestRenderer; let bridge: FrontendBridge; - let legacyRender; let store: Store; let utils; @@ -30,8 +31,6 @@ describe('OwnersListContext', () => { utils = require('./utils'); utils.beforeEachProfiling(); - legacyRender = utils.legacyRender; - bridge = global.bridge; store = global.store; store.collapseNodesByDefault = false; @@ -51,6 +50,8 @@ describe('OwnersListContext', () => { require('react-devtools-shared/src/devtools/views/Components/TreeContext').TreeContextController; }); + const {render} = getVersionedRenderImplementation(); + const Contexts = ({children, defaultOwnerID = null}) => ( @@ -98,9 +99,7 @@ describe('OwnersListContext', () => { }; const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -143,9 +142,7 @@ describe('OwnersListContext', () => { }; const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -171,9 +168,7 @@ describe('OwnersListContext', () => { const Grandparent = () => ; const Parent = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -198,9 +193,7 @@ describe('OwnersListContext', () => { return ; }; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] diff --git a/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js b/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js index 3536034d9f748..cf5304664b815 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerChangeDescriptions-test.js @@ -7,9 +7,10 @@ * @flow */ +import {getVersionedRenderImplementation} from './utils'; + describe('Profiler change descriptions', () => { let React; - let legacyRender; let store; let utils; @@ -17,8 +18,6 @@ describe('Profiler change descriptions', () => { utils = require('./utils'); utils.beforeEachProfiling(); - legacyRender = utils.legacyRender; - store = global.store; store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; @@ -26,6 +25,8 @@ describe('Profiler change descriptions', () => { React = require('react'); }); + const {render} = getVersionedRenderImplementation(); + // @reactVersion >=18.0 it('should identify useContext as the cause for a re-render', () => { const Context = React.createContext(0); @@ -62,10 +63,8 @@ describe('Profiler change descriptions', () => { ); }; - const container = document.createElement('div'); - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); utils.act(() => forceUpdate()); utils.act(() => store.profilerStore.stopProfiling()); diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js index e9339649359de..3b375c5d9832f 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js @@ -9,10 +9,10 @@ import type Store from 'react-devtools-shared/src/devtools/store'; +import {getVersionedRenderImplementation} from './utils'; + describe('ProfilerStore', () => { let React; - let ReactDOM; - let legacyRender; let store: Store; let utils; @@ -20,16 +20,17 @@ describe('ProfilerStore', () => { utils = require('./utils'); utils.beforeEachProfiling(); - legacyRender = utils.legacyRender; - store = global.store; store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; React = require('react'); - ReactDOM = require('react-dom'); }); + const {render, unmount} = getVersionedRenderImplementation(); + const {render: renderOther, unmount: unmountOther} = + getVersionedRenderImplementation(); + // @reactVersion >= 16.9 it('should not remove profiling data when roots are unmounted', async () => { const Parent = ({count}) => @@ -38,19 +39,16 @@ describe('ProfilerStore', () => { .map((_, index) => ); const Child = () =>
Hi!
; - const containerA = document.createElement('div'); - const containerB = document.createElement('div'); - utils.act(() => { - legacyRender(, containerA); - legacyRender(, containerB); + render(); + renderOther(); }); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => { - legacyRender(, containerA); - legacyRender(, containerB); + render(); + renderOther(); }); utils.act(() => store.profilerStore.stopProfiling()); @@ -58,12 +56,10 @@ describe('ProfilerStore', () => { const rootA = store.roots[0]; const rootB = store.roots[1]; - utils.act(() => ReactDOM.unmountComponentAtNode(containerB)); - + utils.act(() => unmountOther()); expect(store.profilerStore.getDataForRoot(rootA)).not.toBeNull(); - utils.act(() => ReactDOM.unmountComponentAtNode(containerA)); - + utils.act(() => unmount()); expect(store.profilerStore.getDataForRoot(rootB)).not.toBeNull(); }); @@ -95,14 +91,9 @@ describe('ProfilerStore', () => { return ; }; - const container = document.createElement('div'); - - // This element has to be in the for the event system to work. - document.body.appendChild(container); - // It's important that this test uses legacy sync mode. // The root API does not trigger this particular failing case. - legacyRender(, container); + utils.act(() => render()); utils.act(() => store.profilerStore.startProfiling()); @@ -148,14 +139,9 @@ describe('ProfilerStore', () => { return ; }; - const container = document.createElement('div'); - - // This element has to be in the for the event system to work. - document.body.appendChild(container); - // It's important that this test uses legacy sync mode. // The root API does not trigger this particular failing case. - legacyRender(, container); + utils.act(() => render()); expect(commitCount).toBe(1); commitCount = 0; @@ -164,10 +150,10 @@ describe('ProfilerStore', () => { // Focus and blur. const target = inputRef.current; - target.focus(); - target.blur(); - target.focus(); - target.blur(); + utils.act(() => target.focus()); + utils.act(() => target.blur()); + utils.act(() => target.focus()); + utils.act(() => target.blur()); expect(commitCount).toBe(1); utils.act(() => store.profilerStore.stopProfiling()); @@ -204,14 +190,9 @@ describe('ProfilerStore', () => { return state.hasOwnProperty; }; - const container = document.createElement('div'); - - // This element has to be in the for the event system to work. - document.body.appendChild(container); - // It's important that this test uses legacy sync mode. // The root API does not trigger this particular failing case. - legacyRender(, container); + utils.act(() => render()); utils.act(() => store.profilerStore.startProfiling()); utils.act(() => @@ -243,9 +224,7 @@ describe('ProfilerStore', () => { ); }; - const container = document.createElement('div'); - - utils.act(() => legacyRender(, container)); + utils.act(() => render()); utils.act(() => store.profilerStore.startProfiling()); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js index d59d616d67e61..41d9093feeb54 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js @@ -9,10 +9,11 @@ import type Store from 'react-devtools-shared/src/devtools/store'; +import {getVersionedRenderImplementation} from './utils'; + describe('profiling charts', () => { let React; let Scheduler; - let legacyRender; let store: Store; let utils; @@ -20,8 +21,6 @@ describe('profiling charts', () => { utils = require('./utils'); utils.beforeEachProfiling(); - legacyRender = utils.legacyRender; - store = global.store; store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; @@ -30,6 +29,8 @@ describe('profiling charts', () => { Scheduler = require('scheduler'); }); + const {render} = getVersionedRenderImplementation(); + function getFlamegraphChartData(rootID, commitIndex) { const commitTree = store.profilerStore.profilingCache.getCommitTree({ commitIndex, @@ -78,11 +79,9 @@ describe('profiling charts', () => { return null; }); - const container = document.createElement('div'); - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -91,7 +90,7 @@ describe('profiling charts', () => { [Memo] `); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -228,11 +227,9 @@ describe('profiling charts', () => { return null; }); - const container = document.createElement('div'); - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -241,7 +238,7 @@ describe('profiling charts', () => { [Memo] `); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index c277f6f167d06..dec3c41c8f1b6 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -7,6 +7,8 @@ * @flow */ +import {getVersionedRenderImplementation} from './utils'; + describe('Store', () => { let React; let ReactDOM; @@ -37,13 +39,13 @@ describe('Store', () => { withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; }); + const {render, unmount, createContainer} = getVersionedRenderImplementation(); + // @reactVersion >= 18.0 it('should not allow a root node to be collapsed', () => { const Component = () =>
Hi
; - act(() => - legacyRender(, document.createElement('div')), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -62,15 +64,13 @@ describe('Store', () => { it('should properly handle a root with no visible nodes', () => { const Root = ({children}) => children; - const container = document.createElement('div'); - - act(() => legacyRender({null}, container)); + act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); - act(() => legacyRender(
, container)); + act(() => render(
)); expect(store).toMatchInlineSnapshot(`[root]`); }); @@ -93,17 +93,14 @@ describe('Store', () => { }; const Child = () => null; - const container = document.createElement('div'); - act(() => - legacyRender( + render( <> , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -119,9 +116,7 @@ describe('Store', () => { const Component = () => null; Component.displayName = '🟩💜🔵'; - const container = document.createElement('div'); - - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> @@ -196,9 +191,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - const container = document.createElement('div'); - - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -214,7 +207,7 @@ describe('Store', () => { `); - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -226,12 +219,13 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(container)); + act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots', () => { + // @reactVersion < 19 + it('should support mount and update operations for multiple roots (legacy render)', () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -285,6 +279,64 @@ describe('Store', () => { expect(store).toMatchInlineSnapshot(``); }); + // @reactVersion >= 18.0 + it('should support mount and update operations for multiple roots (createRoot)', () => { + const Parent = ({count}) => + new Array(count).fill(true).map((_, index) => ); + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + const rootA = ReactDOMClient.createRoot(containerA); + const rootB = ReactDOMClient.createRoot(containerB); + + act(() => { + rootA.render(); + rootB.render(); + }); + expect(store).toMatchInlineSnapshot(` + [root] + â–¾ + + + + [root] + â–¾ + + + `); + + act(() => { + rootA.render(); + rootB.render(); + }); + expect(store).toMatchInlineSnapshot(` + [root] + â–¾ + + + + + [root] + â–¾ + + `); + + act(() => rootB.unmount()); + expect(store).toMatchInlineSnapshot(` + [root] + â–¾ + + + + + `); + + act(() => rootA.unmount()); + expect(store).toMatchInlineSnapshot(``); + }); + // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', () => { const Grandparent = () => ( @@ -302,9 +354,7 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => - legacyRender(, document.createElement('div')), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -337,8 +387,7 @@ describe('Store', () => { ); - const container = document.createElement('div'); - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -348,7 +397,7 @@ describe('Store', () => { `); act(() => { - legacyRender(, container); + render(); }); expect(store).toMatchInlineSnapshot(` [root] @@ -399,15 +448,13 @@ describe('Store', () => { ); - const container = document.createElement('div'); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -425,13 +472,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -449,13 +495,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -473,13 +518,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -497,13 +541,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -514,13 +557,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -538,13 +580,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -599,13 +640,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -658,13 +698,12 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -743,9 +782,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => - legacyRender(, document.createElement('div')), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -816,9 +853,7 @@ describe('Store', () => { const foo = ; const bar = ; - const container = document.createElement('div'); - - act(() => legacyRender({[foo, bar]}, container)); + act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -829,7 +864,7 @@ describe('Store', () => { `); - act(() => legacyRender({[bar, foo]}, container)); + act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -870,15 +905,12 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - const container = document.createElement('div'); - act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -888,12 +920,11 @@ describe('Store', () => { `); act(() => - legacyRender( + render( , - container, ), ); expect(store).toMatchInlineSnapshot(` @@ -902,12 +933,13 @@ describe('Store', () => { â–¸ `); - act(() => ReactDOM.unmountComponentAtNode(container)); + act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots', () => { + // @reactVersion < 19 + it('should support mount and update operations for multiple roots (legacy render)', () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; @@ -947,6 +979,50 @@ describe('Store', () => { expect(store).toMatchInlineSnapshot(``); }); + // @reactVersion >= 18.0 + it('should support mount and update operations for multiple roots (createRoot)', () => { + const Parent = ({count}) => + new Array(count).fill(true).map((_, index) => ); + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + const rootA = ReactDOMClient.createRoot(containerA); + const rootB = ReactDOMClient.createRoot(containerB); + + act(() => { + rootA.render(); + rootB.render(); + }); + expect(store).toMatchInlineSnapshot(` + [root] + â–¸ + [root] + â–¸ + `); + + act(() => { + rootA.render(); + rootB.render(); + }); + expect(store).toMatchInlineSnapshot(` + [root] + â–¸ + [root] + â–¸ + `); + + act(() => rootB.unmount()); + expect(store).toMatchInlineSnapshot(` + [root] + â–¸ + `); + + act(() => rootA.unmount()); + expect(store).toMatchInlineSnapshot(``); + }); + // @reactVersion >= 18.0 it('should filter DOM nodes from the store tree', () => { const Grandparent = () => ( @@ -964,9 +1040,7 @@ describe('Store', () => { ); const Child = () =>
Hi!
; - act(() => - legacyRender(, document.createElement('div')), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ @@ -1012,8 +1086,7 @@ describe('Store', () => { ); - const container = document.createElement('div'); - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¸ @@ -1031,7 +1104,7 @@ describe('Store', () => { `); act(() => { - legacyRender(, container); + render(); }); expect(store).toMatchInlineSnapshot(` [root] @@ -1054,9 +1127,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
Hi!
; - act(() => - legacyRender(, document.createElement('div')), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1136,12 +1207,7 @@ describe('Store', () => { const ref = React.createRef(); - act(() => - legacyRender( - , - document.createElement('div'), - ), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1207,15 +1273,13 @@ describe('Store', () => { const foo = ; const bar = ; - const container = document.createElement('div'); - - act(() => legacyRender({[foo, bar]}, container)); + act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => legacyRender({[bar, foo]}, container)); + act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1264,7 +1328,7 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1328,7 +1392,7 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); @@ -1342,8 +1406,8 @@ describe('Store', () => { const Child = () => null; act(() => { - legacyRender(, document.createElement('div')); - legacyRender(, document.createElement('div')); + render(); + render(); }); for (let i = 0; i < store.numElements; i++) { @@ -1358,12 +1422,11 @@ describe('Store', () => { const Child = () => null; act(() => - legacyRender( + render( , - document.createElement('div'), ), ); @@ -1379,19 +1442,20 @@ describe('Store', () => { const Child = () => null; act(() => { - legacyRender( + render( , - document.createElement('div'), ); - legacyRender( + + createContainer(); + + render( , - document.createElement('div'), ); }); @@ -1402,7 +1466,8 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('detects and updates profiling support based on the attached roots', () => { + // @reactVersion < 19 + it('detects and updates profiling support based on the attached roots (legacy render)', () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1421,6 +1486,29 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); }); + // @reactVersion >= 18 + it('detects and updates profiling support based on the attached roots (createRoot)', () => { + const Component = () => null; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + const rootA = ReactDOMClient.createRoot(containerA); + const rootB = ReactDOMClient.createRoot(containerB); + + expect(store.rootSupportsBasicProfiling).toBe(false); + + act(() => rootA.render()); + expect(store.rootSupportsBasicProfiling).toBe(true); + + act(() => rootB.render()); + act(() => rootA.unmount()); + expect(store.rootSupportsBasicProfiling).toBe(true); + + act(() => rootB.unmount()); + expect(store.rootSupportsBasicProfiling).toBe(false); + }); + // @reactVersion >= 18.0 it('should properly serialize non-string key values', () => { const Child = () => null; @@ -1429,7 +1517,7 @@ describe('Store', () => { // This is pretty hacky. const fauxElement = Object.assign({}, , {key: 123}); - act(() => legacyRender([fauxElement], document.createElement('div'))); + act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] @@ -1488,15 +1576,13 @@ describe('Store', () => { ); - const container = document.createElement('div'); - // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + act(() => render()); await Promise.resolve(); // Render again after it resolves - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1543,6 +1629,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 + // @reactVersion < 19 it('should support Lazy components (legacy render)', async () => { const container = document.createElement('div'); @@ -1612,6 +1699,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 + // @reactVersion < 19 it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => { const container = document.createElement('div'); @@ -1634,6 +1722,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 + // @reactVersion < 19 it('should support Lazy components that are unmounted before they finish loading in (createRoot)', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -1665,10 +1754,9 @@ describe('Store', () => { console.warn('test-only: render warning'); return null; } - const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => legacyRender(, container)); + act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1678,7 +1766,7 @@ describe('Store', () => { `); withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => legacyRender(, container)); + act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1697,10 +1785,9 @@ describe('Store', () => { }); return null; } - const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => legacyRender(, container)); + act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1710,7 +1797,7 @@ describe('Store', () => { `); withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => legacyRender(, container)); + act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1739,11 +1826,10 @@ describe('Store', () => { }); return null; } - const container = document.createElement('div'); withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { - legacyRender(, container); + render(); }, false); }); flushPendingBridgeOperations(); @@ -1760,7 +1846,7 @@ describe('Store', () => { ✕⚠ `); - act(() => ReactDOM.unmountComponentAtNode(container)); + act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); @@ -1778,15 +1864,12 @@ describe('Store', () => { return null; } - const container = document.createElement('div'); - withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => { - legacyRender( + render( <> , - container, ); }, false); flushPendingBridgeOperations(); @@ -1797,12 +1880,11 @@ describe('Store', () => { // Before warnings and errors have flushed, flush another commit. act(() => { - legacyRender( + render( <> , - container, ); }, false); flushPendingBridgeOperations(); @@ -1823,14 +1905,13 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(container)); + act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); // @reactVersion >= 18.0 it('from react get counted', () => { - const container = document.createElement('div'); function Example() { return []; } @@ -1841,7 +1922,7 @@ describe('Store', () => { withErrorsOrWarningsIgnored( ['Warning: Each child in a list should have a unique "key" prop'], () => { - act(() => legacyRender(, container)); + act(() => render()); }, ); @@ -1860,15 +1941,14 @@ describe('Store', () => { console.warn('test-only: render warning'); return null; } - const container = document.createElement('div'); + withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -1902,15 +1982,14 @@ describe('Store', () => { console.warn('test-only: render warning'); return null; } - const container = document.createElement('div'); + withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -1948,15 +2027,14 @@ describe('Store', () => { console.warn('test-only: render warning'); return null; } - const container = document.createElement('div'); + withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -2002,16 +2080,15 @@ describe('Store', () => { console.warn('test-only: render warning'); return null; } - const container = document.createElement('div'); + withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -2025,12 +2102,11 @@ describe('Store', () => { withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -2043,11 +2119,10 @@ describe('Store', () => { withErrorsOrWarningsIgnored(['test-only:'], () => { act(() => - legacyRender( + render( , - container, ), ); }); @@ -2058,7 +2133,7 @@ describe('Store', () => { `); withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => legacyRender(, container)); + act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.errorCount).toBe(0); @@ -2086,9 +2161,7 @@ describe('Store', () => { ); } - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await actAsync(() => root.render()); + await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -2097,7 +2170,7 @@ describe('Store', () => { `); - await actAsync(() => root.render()); + await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` [root] diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 44c7e85217cfd..e2a59177b687f 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -19,7 +19,9 @@ describe('Store component filters', () => { let utils; const act = async (callback: Function) => { - if (React.unstable_act != null) { + if (React.act != null) { + await React.act(callback); + } else if (React.unstable_act != null) { await React.unstable_act(callback); } else { callback(); diff --git a/packages/react-devtools-shared/src/__tests__/storeOwners-test.js b/packages/react-devtools-shared/src/__tests__/storeOwners-test.js index df388f69a6c06..f4fd6ba2ba826 100644 --- a/packages/react-devtools-shared/src/__tests__/storeOwners-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeOwners-test.js @@ -8,11 +8,11 @@ */ const {printOwnersList} = require('../devtools/utils'); +const {getVersionedRenderImplementation} = require('./utils'); describe('Store owners list', () => { let React; let act; - let legacyRender; let store; beforeEach(() => { @@ -23,9 +23,10 @@ describe('Store owners list', () => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; }); + const {render} = getVersionedRenderImplementation(); + function getFormattedOwnersList(elementID) { const ownersList = store.getOwnersListForElement(elementID); return printOwnersList(ownersList); @@ -43,7 +44,7 @@ describe('Store owners list', () => { const Leaf = () =>
Leaf
; const Intermediate = ({children}) => {children}; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -80,7 +81,7 @@ describe('Store owners list', () => { {children}, ]; - act(() => legacyRender(, document.createElement('div'))); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -122,14 +123,7 @@ describe('Store owners list', () => { const Leaf = () =>
Leaf
; const Intermediate = ({children}) => {children}; - const container = document.createElement('div'); - - act(() => - legacyRender( - , - container, - ), - ); + act(() => render()); const rootID = store.getElementIDAtIndex(0); expect(store).toMatchInlineSnapshot(` @@ -145,12 +139,7 @@ describe('Store owners list', () => { " `); - act(() => - legacyRender( - , - container, - ), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -166,12 +155,7 @@ describe('Store owners list', () => { " `); - act(() => - legacyRender( - , - container, - ), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] â–¾ @@ -182,12 +166,7 @@ describe('Store owners list', () => { " `); - act(() => - legacyRender( - , - container, - ), - ); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -204,8 +183,7 @@ describe('Store owners list', () => { : [, , ]; const Leaf = () =>
Leaf
; - const container = document.createElement('div'); - act(() => legacyRender(, container)); + act(() => render()); const rootID = store.getElementIDAtIndex(0); expect(store).toMatchInlineSnapshot(` @@ -222,7 +200,7 @@ describe('Store owners list', () => { " `); - act(() => legacyRender(, container)); + act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js index 38b38a47e4df3..894524762ac88 100644 --- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js @@ -15,12 +15,12 @@ import type { StateContext, } from 'react-devtools-shared/src/devtools/views/Components/TreeContext'; +import {getVersionedRenderImplementation} from './utils'; + describe('TreeListContext', () => { let React; - let ReactDOM; let TestRenderer: ReactTestRenderer; let bridge: FrontendBridge; - let legacyRender; let store: Store; let utils; let withErrorsOrWarningsIgnored; @@ -36,7 +36,6 @@ describe('TreeListContext', () => { utils = require('./utils'); utils.beforeEachProfiling(); - legacyRender = utils.legacyRender; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; bridge = global.bridge; @@ -44,7 +43,6 @@ describe('TreeListContext', () => { store.collapseNodesByDefault = false; React = require('react'); - ReactDOM = require('react-dom'); TestRenderer = utils.requireTestRenderer(); BridgeContext = @@ -54,6 +52,8 @@ describe('TreeListContext', () => { TreeContext = require('react-devtools-shared/src/devtools/views/Components/TreeContext'); }); + const {render, unmount, createContainer} = getVersionedRenderImplementation(); + afterEach(() => { // Reset between tests dispatch = ((null: any): DispatcherContext); @@ -89,9 +89,7 @@ describe('TreeListContext', () => { ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -215,9 +213,7 @@ describe('TreeListContext', () => { ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -301,9 +297,7 @@ describe('TreeListContext', () => { ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -391,16 +385,14 @@ describe('TreeListContext', () => { const Parent = props => props.children || null; const Child = () => null; - const container = document.createElement('div'); utils.act(() => - legacyRender( + render( , - container, ), ); @@ -427,11 +419,10 @@ describe('TreeListContext', () => { // Remove the child (which should auto-select the parent) await utils.actAsync(() => - legacyRender( + render( , - container, ), ); expect(state).toMatchInlineSnapshot(` @@ -441,7 +432,7 @@ describe('TreeListContext', () => { `); // Unmount the root (so that nothing is selected) - await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container)); + await utils.actAsync(() => unmount()); expect(state).toMatchInlineSnapshot(``); }); @@ -459,9 +450,7 @@ describe('TreeListContext', () => { .map((_, index) => ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -620,9 +609,7 @@ describe('TreeListContext', () => { .map((_, index) => ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -920,14 +907,13 @@ describe('TreeListContext', () => { Qux.displayName = `withHOC(${Qux.name})`; utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ); @@ -992,14 +978,13 @@ describe('TreeListContext', () => { const Baz = () => null; utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ); @@ -1096,15 +1081,12 @@ describe('TreeListContext', () => { const Bar = () => null; const Baz = () => null; - const container = document.createElement('div'); - utils.act(() => - legacyRender( + render( , - container, ), ); @@ -1125,13 +1107,12 @@ describe('TreeListContext', () => { `); await utils.actAsync(() => - legacyRender( + render( , - container, ), ); utils.act(() => renderer.update()); @@ -1157,16 +1138,13 @@ describe('TreeListContext', () => { const Bar = () => null; const Baz = () => null; - const container = document.createElement('div'); - utils.act(() => - legacyRender( + render( , - container, ), ); @@ -1198,12 +1176,11 @@ describe('TreeListContext', () => { `); await utils.actAsync(() => - legacyRender( + render( , - container, ), ); utils.act(() => renderer.update()); @@ -1243,9 +1220,7 @@ describe('TreeListContext', () => { ); const Child = () => null; - utils.act(() => - legacyRender(, document.createElement('div')), - ); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -1284,8 +1259,7 @@ describe('TreeListContext', () => { new Array(count).fill(true).map((_, index) => ); const Child = () => null; - const container = document.createElement('div'); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -1307,18 +1281,14 @@ describe('TreeListContext', () => { `); - await utils.actAsync(() => - legacyRender(, container), - ); + await utils.actAsync(() => render()); expect(state).toMatchInlineSnapshot(` [owners] → ▾ `); - await utils.actAsync(() => - legacyRender(, container), - ); + await utils.actAsync(() => render()); expect(state).toMatchInlineSnapshot(` [owners] → @@ -1329,13 +1299,11 @@ describe('TreeListContext', () => { const Parent = props => props.children || null; const Child = () => null; - const container = document.createElement('div'); utils.act(() => - legacyRender( + render( , - container, ), ); @@ -1355,7 +1323,7 @@ describe('TreeListContext', () => { → `); - await utils.actAsync(() => legacyRender(, container)); + await utils.actAsync(() => render()); expect(state).toMatchInlineSnapshot(` [root] → @@ -1369,7 +1337,7 @@ describe('TreeListContext', () => { → `); - await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container)); + await utils.actAsync(() => unmount()); expect(state).toMatchInlineSnapshot(``); }); @@ -1387,8 +1355,7 @@ describe('TreeListContext', () => { ); - const container = document.createElement('div'); - utils.act(() => legacyRender(, container)); + utils.act(() => render()); let renderer; utils.act(() => (renderer = TestRenderer.create())); @@ -1495,13 +1462,12 @@ describe('TreeListContext', () => { it('should handle when there are no errors/warnings', () => { utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ); @@ -1558,7 +1524,7 @@ describe('TreeListContext', () => { it('should cycle through the next errors/warnings and wrap around', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -1566,7 +1532,6 @@ describe('TreeListContext', () => { , - document.createElement('div'), ), ), ); @@ -1619,7 +1584,7 @@ describe('TreeListContext', () => { it('should cycle through the previous errors/warnings and wrap around', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -1627,7 +1592,6 @@ describe('TreeListContext', () => { , - document.createElement('div'), ), ), ); @@ -1680,20 +1644,21 @@ describe('TreeListContext', () => { it('should cycle through the next errors/warnings and wrap around with multiple roots', () => { withErrorsOrWarningsIgnored(['test-only:'], () => { utils.act(() => { - legacyRender( + render( , , - document.createElement('div'), ); - legacyRender( + + createContainer(); + + render( , - document.createElement('div'), ); }); }); @@ -1750,20 +1715,21 @@ describe('TreeListContext', () => { it('should cycle through the previous errors/warnings and wrap around with multiple roots', () => { withErrorsOrWarningsIgnored(['test-only:'], () => { utils.act(() => { - legacyRender( + render( , , - document.createElement('div'), ); - legacyRender( + + createContainer(); + + render( , - document.createElement('div'), ); }); }); @@ -1820,7 +1786,7 @@ describe('TreeListContext', () => { it('should select the next or previous element relative to the current selection', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -1828,7 +1794,6 @@ describe('TreeListContext', () => { , - document.createElement('div'), ), ), ); @@ -1882,14 +1847,13 @@ describe('TreeListContext', () => { it('should update correctly when errors/warnings are cleared for a fiber in the list', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ), ); @@ -1955,12 +1919,11 @@ describe('TreeListContext', () => { it('should update correctly when errors/warnings are cleared for the currently selected fiber', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ), ); @@ -1992,18 +1955,15 @@ describe('TreeListContext', () => { }); it('should update correctly when new errors/warnings are added', () => { - const container = document.createElement('div'); - withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2030,14 +1990,13 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2076,12 +2035,11 @@ describe('TreeListContext', () => { it('should update correctly when all errors/warnings are cleared', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ), ); @@ -2120,7 +2078,6 @@ describe('TreeListContext', () => { }); it('should update correctly when elements are added/removed', () => { - const container = document.createElement('div'); let errored = false; function ErrorOnce() { if (!errored) { @@ -2131,11 +2088,10 @@ describe('TreeListContext', () => { } withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2150,12 +2106,11 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2178,7 +2133,6 @@ describe('TreeListContext', () => { }); it('should update correctly when elements are re-ordered', () => { - const container = document.createElement('div'); function ErrorOnce() { const didErrorRef = React.useRef(false); if (!didErrorRef.current) { @@ -2189,14 +2143,13 @@ describe('TreeListContext', () => { } withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2227,14 +2180,13 @@ describe('TreeListContext', () => { // Re-order the tree and ensure indices are updated. withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2261,14 +2213,13 @@ describe('TreeListContext', () => { // Re-order the tree and ensure indices are updated. withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2289,7 +2240,7 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -2300,7 +2251,6 @@ describe('TreeListContext', () => {
, - document.createElement('div'), ), ), ); @@ -2339,7 +2289,7 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -2350,7 +2300,6 @@ describe('TreeListContext', () => {
, - document.createElement('div'), ), ), ); @@ -2425,7 +2374,7 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( @@ -2436,7 +2385,6 @@ describe('TreeListContext', () => {
, - document.createElement('div'), ), ), ); @@ -2483,12 +2431,11 @@ describe('TreeListContext', () => { withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - document.createElement('div'), ), ), ); @@ -2515,16 +2462,13 @@ describe('TreeListContext', () => { } const LazyComponent = React.lazy(() => fakeImport(Child)); - const container = document.createElement('div'); - withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2538,12 +2482,11 @@ describe('TreeListContext', () => { await Promise.resolve(); withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( , - container, ), ), ); @@ -2565,15 +2508,12 @@ describe('TreeListContext', () => { const Fallback = () => ; - const container = document.createElement('div'); - withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( }> , - container, ), ), ); @@ -2590,11 +2530,10 @@ describe('TreeListContext', () => { await Promise.resolve(); withErrorsOrWarningsIgnored(['test-only:'], () => utils.act(() => - legacyRender( + render( }> , - container, ), ), ); @@ -2629,16 +2568,14 @@ describe('TreeListContext', () => { } } - const container = document.createElement('div'); withErrorsOrWarningsIgnored( ['test-only:', 'React will try to recreate this component tree'], () => { utils.act(() => - legacyRender( + render( , - container, ), ); }, @@ -2659,7 +2596,7 @@ describe('TreeListContext', () => { → ✕ `); - utils.act(() => ReactDOM.unmountComponentAtNode(container)); + utils.act(() => unmount()); expect(state).toMatchInlineSnapshot(``); // Should be a noop @@ -2693,16 +2630,14 @@ describe('TreeListContext', () => { } } - const container = document.createElement('div'); withErrorsOrWarningsIgnored( ['test-only:', 'React will try to recreate this component tree'], () => { utils.act(() => - legacyRender( + render( , - container, ), ); }, @@ -2723,7 +2658,7 @@ describe('TreeListContext', () => { → ✕ `); - utils.act(() => ReactDOM.unmountComponentAtNode(container)); + utils.act(() => unmount()); expect(state).toMatchInlineSnapshot(``); // Should be a noop @@ -2752,16 +2687,14 @@ describe('TreeListContext', () => { } } - const container = document.createElement('div'); withErrorsOrWarningsIgnored( ['test-only:', 'React will try to recreate this component tree'], () => { utils.act(() => - legacyRender( + render( , - container, ), ); }, diff --git a/packages/react-devtools-shared/src/__tests__/useEditableValue-test.js b/packages/react-devtools-shared/src/__tests__/useEditableValue-test.js index d3ae11df7999f..61b538e2d2e6f 100644 --- a/packages/react-devtools-shared/src/__tests__/useEditableValue-test.js +++ b/packages/react-devtools-shared/src/__tests__/useEditableValue-test.js @@ -7,22 +7,24 @@ * @flow */ +import {getVersionedRenderImplementation} from './utils'; + describe('useEditableValue', () => { let act; let React; - let legacyRender; let useEditableValue; beforeEach(() => { const utils = require('./utils'); act = utils.act; - legacyRender = utils.legacyRender; React = require('react'); useEditableValue = require('../devtools/views/hooks').useEditableValue; }); + const {render} = getVersionedRenderImplementation(); + it('should not cause a loop with values like NaN', () => { let state; @@ -32,8 +34,8 @@ describe('useEditableValue', () => { return null; } - const container = document.createElement('div'); - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('NaN'); expect(state.externalValue).toEqual(NaN); expect(state.parsedValue).toEqual(NaN); @@ -50,8 +52,8 @@ describe('useEditableValue', () => { return null; } - const container = document.createElement('div'); - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('1'); expect(state.externalValue).toEqual(1); expect(state.parsedValue).toEqual(1); @@ -60,7 +62,8 @@ describe('useEditableValue', () => { // If there are NO pending changes, // an update to the external prop value should override the local/pending value. - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('2'); expect(state.externalValue).toEqual(2); expect(state.parsedValue).toEqual(2); @@ -78,8 +81,8 @@ describe('useEditableValue', () => { return null; } - const container = document.createElement('div'); - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('1'); expect(state.externalValue).toEqual(1); expect(state.parsedValue).toEqual(1); @@ -102,7 +105,8 @@ describe('useEditableValue', () => { // If there ARE pending changes, // an update to the external prop value should NOT override the local/pending value. - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('2'); expect(state.externalValue).toEqual(3); expect(state.parsedValue).toEqual(2); @@ -120,8 +124,8 @@ describe('useEditableValue', () => { return null; } - const container = document.createElement('div'); - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('1'); expect(state.externalValue).toEqual(1); expect(state.parsedValue).toEqual(1); @@ -153,8 +157,8 @@ describe('useEditableValue', () => { return null; } - const container = document.createElement('div'); - legacyRender(, container); + act(() => render()); + expect(state.editableValue).toEqual('1'); expect(state.externalValue).toEqual(1); expect(state.parsedValue).toEqual(1); diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index 1a74c1fee9c61..8555302507ff5 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -7,21 +7,48 @@ * @flow */ +import semver from 'semver'; + import typeof ReactTestRenderer from 'react-test-renderer'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type Store from 'react-devtools-shared/src/devtools/store'; import type {ProfilingDataFrontend} from 'react-devtools-shared/src/devtools/views/Profiler/types'; import type {ElementType} from 'react-devtools-shared/src/frontend/types'; +import type {Node as ReactNode} from 'react'; + +import {ReactVersion} from '../../../../ReactVersions'; + +const requestedReactVersion = process.env.REACT_VERSION || ReactVersion; +export function getActDOMImplementation(): () => void | Promise { + // This is for React < 18, where act was distributed in react-dom/test-utils. + if (semver.lt(requestedReactVersion, '18.0.0')) { + const ReactDOMTestUtils = require('react-dom/test-utils'); + return ReactDOMTestUtils.act; + } + + const React = require('react'); + // This is for React 18, where act was distributed in react as unstable. + if (React.unstable_act) { + return React.unstable_act; + } + + // This is for React > 18, where act is marked as stable. + if (React.act) { + return React.act; + } + + throw new Error("Couldn't find any available act implementation"); +} export function act( callback: Function, recursivelyFlush: boolean = true, ): void { + // act from react-test-renderer has some side effects on React DevTools + // it injects the renderer for DevTools, see ReactTestRenderer.js const {act: actTestRenderer} = require('react-test-renderer'); - // Use `require('react-dom/test-utils').act` as a fallback for React 17, which can be used in integration tests for React DevTools. - const actDOM = - require('react').unstable_act || require('react-dom/test-utils').act; + const actDOM = getActDOMImplementation(); actDOM(() => { actTestRenderer(() => { @@ -45,10 +72,10 @@ export async function actAsync( cb: () => *, recursivelyFlush: boolean = true, ): Promise { + // act from react-test-renderer has some side effects on React DevTools + // it injects the renderer for DevTools, see ReactTestRenderer.js const {act: actTestRenderer} = require('react-test-renderer'); - // Use `require('react-dom/test-utils').act` as a fallback for React 17, which can be used in integration tests for React DevTools. - const actDOM = - require('react').unstable_act || require('react-dom/test-utils').act; + const actDOM = getActDOMImplementation(); await actDOM(async () => { await actTestRenderer(async () => { @@ -73,6 +100,123 @@ export async function actAsync( } } +type RenderImplementation = { + render: (elements: ?ReactNode) => () => void, + unmount: () => void, + createContainer: () => void, + getContainer: () => ?HTMLElement, +}; + +export function getLegacyRenderImplementation(): RenderImplementation { + let ReactDOM; + let container; + const containersToRemove = []; + + beforeEach(() => { + ReactDOM = require('react-dom'); + + createContainer(); + }); + + afterEach(() => { + containersToRemove.forEach(c => document.body.removeChild(c)); + containersToRemove.splice(0, containersToRemove.length); + + ReactDOM = null; + container = null; + }); + + function render(elements) { + withErrorsOrWarningsIgnored( + ['ReactDOM.render is no longer supported in React 18'], + () => { + ReactDOM.render(elements, container); + }, + ); + + return unmount; + } + + function unmount() { + ReactDOM.unmountComponentAtNode(container); + } + + function createContainer() { + container = document.createElement('div'); + document.body.appendChild(container); + + containersToRemove.push(container); + } + + function getContainer() { + return container; + } + + return { + render, + unmount, + createContainer, + getContainer, + }; +} + +export function getModernRenderImplementation(): RenderImplementation { + let ReactDOMClient; + let container; + let root; + const containersToRemove = []; + + beforeEach(() => { + ReactDOMClient = require('react-dom/client'); + + createContainer(); + }); + + afterEach(() => { + containersToRemove.forEach(c => document.body.removeChild(c)); + containersToRemove.splice(0, containersToRemove.length); + + ReactDOMClient = null; + container = null; + root = null; + }); + + function render(elements) { + root.render(elements); + + return unmount; + } + + function unmount() { + root.unmount(); + } + + function createContainer() { + container = document.createElement('div'); + document.body.appendChild(container); + + root = ReactDOMClient.createRoot(container); + + containersToRemove.push(container); + } + + function getContainer() { + return container; + } + + return { + render, + unmount, + createContainer, + getContainer, + }; +} + +export const getVersionedRenderImplementation: () => RenderImplementation = + semver.lt(requestedReactVersion, '18.0.0') + ? getLegacyRenderImplementation + : getModernRenderImplementation; + export function beforeEachProfiling(): void { // Mock React's timing information so that test runs are predictable. jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index 939b2fa20e0dd..2709b72ac2bef 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -17,8 +17,10 @@ import { useContext, useDebugValue, useEffect, + useOptimistic, useState, } from 'react'; +import {useFormState} from 'react-dom'; const object = { string: 'abc', @@ -73,6 +75,7 @@ function FunctionWithHooks(props: any, ref: React$Ref) { const [count, updateCount] = useState(0); // eslint-disable-next-line no-unused-vars const contextValueA = useContext(ContextA); + useOptimistic(1); // eslint-disable-next-line no-unused-vars const [_, __] = useState(object); @@ -115,6 +118,18 @@ function wrapWithHoc(Component: (props: any, ref: React$Ref) => any) { } const HocWithHooks = wrapWithHoc(FunctionWithHooks); +function Forms() { + const [state, formAction] = useFormState((n: number, formData: FormData) => { + return n + 1; + }, 0); + return ( +
+ {state} + +
+ ); +} + export default function CustomHooks(): React.Node { return ( @@ -122,6 +137,7 @@ export default function CustomHooks(): React.Node { + ); } diff --git a/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js b/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js index 433d6a3ad3e2b..83679786cbbd2 100644 --- a/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js @@ -10,8 +10,9 @@ 'use strict'; const React = require('react'); -const ReactDOM = require('react-dom'); +const ReactDOMClient = require('react-dom/client'); const ReactDOMServer = require('react-dom/server'); +const act = require('internal-test-utils').act; describe('CSSPropertyOperations', () => { it('should automatically append `px` to relevant styles', () => { @@ -66,15 +67,19 @@ describe('CSSPropertyOperations', () => { expect(html).toContain('"--someColor:#000000"'); }); - it('should set style attribute when styles exist', () => { + it('should set style attribute when styles exist', async () => { const styles = { backgroundColor: '#000', display: 'none', }; - let div =
; - const root = document.createElement('div'); - div = ReactDOM.render(div, root); - expect(/style=".*"/.test(root.innerHTML)).toBe(true); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); + + const div = container.firstChild; + expect(/style=".*"/.test(container.innerHTML)).toBe(true); }); it('should not set style attribute when no styles exist', () => { @@ -87,7 +92,7 @@ describe('CSSPropertyOperations', () => { expect(/style=/.test(html)).toBe(false); }); - it('should warn when using hyphenated style names', () => { + it('should warn when using hyphenated style names', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -96,16 +101,20 @@ describe('CSSPropertyOperations', () => { } } - const root = document.createElement('div'); - - expect(() => ReactDOM.render(, root)).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: Unsupported style property background-color. Did you mean backgroundColor?' + '\n in div (at **)' + '\n in Comp (at **)', ); }); - it('should warn when updating hyphenated style names', () => { + it('should warn when updating hyphenated style names', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -118,10 +127,16 @@ describe('CSSPropertyOperations', () => { '-ms-transform': 'translate3d(0, 0, 0)', '-webkit-transform': 'translate3d(0, 0, 0)', }; - const root = document.createElement('div'); - ReactDOM.render(, root); - - expect(() => ReactDOM.render(, root)).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev([ 'Warning: Unsupported style property -ms-transform. Did you mean msTransform?' + '\n in div (at **)' + '\n in Comp (at **)', @@ -131,7 +146,7 @@ describe('CSSPropertyOperations', () => { ]); }); - it('warns when miscapitalizing vendored style names', () => { + it('warns when miscapitalizing vendored style names', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -148,9 +163,13 @@ describe('CSSPropertyOperations', () => { } } - const root = document.createElement('div'); - - expect(() => ReactDOM.render(, root)).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev([ // msTransform is correct already and shouldn't warn 'Warning: Unsupported vendor-prefixed style property oTransform. ' + 'Did you mean OTransform?' + @@ -163,7 +182,7 @@ describe('CSSPropertyOperations', () => { ]); }); - it('should warn about style having a trailing semicolon', () => { + it('should warn about style having a trailing semicolon', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -181,9 +200,13 @@ describe('CSSPropertyOperations', () => { } } - const root = document.createElement('div'); - - expect(() => ReactDOM.render(, root)).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev([ "Warning: Style property values shouldn't contain a semicolon. " + 'Try "backgroundColor: blue" instead.' + '\n in div (at **)' + @@ -195,7 +218,7 @@ describe('CSSPropertyOperations', () => { ]); }); - it('should warn about style containing a NaN value', () => { + it('should warn about style containing a NaN value', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -204,27 +227,34 @@ describe('CSSPropertyOperations', () => { } } - const root = document.createElement('div'); - - expect(() => ReactDOM.render(, root)).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: `NaN` is an invalid value for the `fontSize` css style property.' + '\n in div (at **)' + '\n in Comp (at **)', ); }); - it('should not warn when setting CSS custom properties', () => { + it('should not warn when setting CSS custom properties', async () => { class Comp extends React.Component { render() { return
; } } - const root = document.createElement('div'); - ReactDOM.render(, root); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); }); - it('should warn about style containing an Infinity value', () => { + it('should warn about style containing an Infinity value', async () => { class Comp extends React.Component { static displayName = 'Comp'; @@ -233,25 +263,32 @@ describe('CSSPropertyOperations', () => { } } - const root = document.createElement('div'); - - expect(() => ReactDOM.render(, root)).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: `Infinity` is an invalid value for the `fontSize` css style property.' + '\n in div (at **)' + '\n in Comp (at **)', ); }); - it('should not add units to CSS custom properties', () => { + it('should not add units to CSS custom properties', async () => { class Comp extends React.Component { render() { return
; } } - const root = document.createElement('div'); - ReactDOM.render(, root); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); - expect(root.children[0].style.getPropertyValue('--foo')).toEqual('5'); + expect(container.children[0].style.getPropertyValue('--foo')).toEqual('5'); }); }); diff --git a/packages/react-dom/src/__tests__/InvalidEventListeners-test.js b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js index c5df3e1196ff5..e7c9b5f9610ca 100644 --- a/packages/react-dom/src/__tests__/InvalidEventListeners-test.js +++ b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js @@ -13,13 +13,15 @@ jest.mock('react-dom-bindings/src/events/isEventSupported'); describe('InvalidEventListeners', () => { let React; - let ReactDOM; + let ReactDOMClient; + let act; let container; beforeEach(() => { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; container = document.createElement('div'); document.body.appendChild(container); @@ -30,13 +32,16 @@ describe('InvalidEventListeners', () => { container = null; }); - it('should prevent non-function listeners, at dispatch', () => { - let node; - expect(() => { - node = ReactDOM.render(
, container); + it('should prevent non-function listeners, at dispatch', async () => { + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(
); + }); }).toErrorDev( 'Expected `onClick` listener to be a function, instead got a value of `string` type.', ); + const node = container.firstChild; spyOnProd(console, 'error'); @@ -46,11 +51,13 @@ describe('InvalidEventListeners', () => { } window.addEventListener('error', handleWindowError); try { - node.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ); + await act(() => { + node.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + }), + ); + }); } finally { window.removeEventListener('error', handleWindowError); } @@ -77,12 +84,19 @@ describe('InvalidEventListeners', () => { } }); - it('should not prevent null listeners, at dispatch', () => { - const node = ReactDOM.render(
, container); - node.dispatchEvent( - new MouseEvent('click', { - bubbles: true, - }), - ); + it('should not prevent null listeners, at dispatch', async () => { + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
); + }); + + const node = container.firstChild; + await act(() => { + node.dispatchEvent( + new MouseEvent('click', { + bubbles: true, + }), + ); + }); }); }); diff --git a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js index 08d81f72aee91..ef584a7856921 100644 --- a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js +++ b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js @@ -13,14 +13,16 @@ 'use strict'; let React; -let ReactTestUtils; +let ReactDOMClient; +let act; describe('ReactChildReconciler', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactTestUtils = require('react-dom/test-utils'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; }); function createIterable(array) { @@ -55,29 +57,39 @@ describe('ReactChildReconciler', () => { return fn; } - it('does not treat functions as iterables', () => { - let node; + it('does not treat functions as iterables', async () => { const iterableFunction = makeIterableFunction('foo'); - expect(() => { - node = ReactTestUtils.renderIntoDocument( -
-

{iterableFunction}

-
, - ); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render( +
+

{iterableFunction}

+
, + ); + }); }).toErrorDev('Functions are not valid as a React child'); + const node = container.firstChild; expect(node.innerHTML).toContain(''); // h1 }); - it('warns for duplicated array keys', () => { + it('warns for duplicated array keys', async () => { class Component extends React.Component { render() { return
{[
,
]}
; } } - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + @@ -85,7 +97,7 @@ describe('ReactChildReconciler', () => { ); }); - it('warns for duplicated array keys with component stack info', () => { + it('warns for duplicated array keys with component stack info', async () => { class Component extends React.Component { render() { return
{[
,
]}
; @@ -104,7 +116,13 @@ describe('ReactChildReconciler', () => { } } - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Encountered two children with the same key, `1`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + @@ -117,14 +135,20 @@ describe('ReactChildReconciler', () => { ); }); - it('warns for duplicated iterable keys', () => { + it('warns for duplicated iterable keys', async () => { class Component extends React.Component { render() { return
{createIterable([
,
])}
; } } - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + @@ -132,7 +156,7 @@ describe('ReactChildReconciler', () => { ); }); - it('warns for duplicated iterable keys with component stack info', () => { + it('warns for duplicated iterable keys with component stack info', async () => { class Component extends React.Component { render() { return
{createIterable([
,
])}
; @@ -151,7 +175,13 @@ describe('ReactChildReconciler', () => { } } - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Encountered two children with the same key, `1`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js index 2d467f82c0d7c..a66f115624e65 100644 --- a/packages/react-dom/src/__tests__/ReactComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactComponent-test.js @@ -11,8 +11,9 @@ let React; let ReactDOM; +let ReactDOMClient; let ReactDOMServer; -let ReactTestUtils; +let act; describe('ReactComponent', () => { beforeEach(() => { @@ -20,11 +21,12 @@ describe('ReactComponent', () => { React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - ReactTestUtils = require('react-dom/test-utils'); + act = require('internal-test-utils').act; }); - it('should throw on invalid render targets', () => { + it('should throw on invalid render targets in legacy roots', () => { const container = document.createElement('div'); // jQuery objects are basically arrays; people often pass them in by mistake expect(function () { @@ -36,40 +38,52 @@ describe('ReactComponent', () => { }).toThrowError(/Target container is not a DOM element./); }); - it('should throw when supplying a string ref outside of render method', () => { - let instance =
; - expect(function () { - instance = ReactTestUtils.renderIntoDocument(instance); - }).toThrow(); + it('should throw when supplying a string ref outside of render method', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render(
); + }), + ).rejects.toThrow(); }); - it('should throw (in dev) when children are mutated during render', () => { + it('should throw (in dev) when children are mutated during render', async () => { function Wrapper(props) { props.children[1] =

; // Mutation is illegal return

{props.children}
; } if (__DEV__) { - expect(() => { - ReactTestUtils.renderIntoDocument( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render( + + + + + , + ); + }), + ).rejects.toThrowError(/Cannot assign to read only property.*/); + } else { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( , ); - }).toThrowError(/Cannot assign to read only property.*/); - } else { - ReactTestUtils.renderIntoDocument( - - - - - , - ); + }); } }); - it('should throw (in dev) when children are mutated during update', () => { + it('should throw (in dev) when children are mutated during update', async () => { class Wrapper extends React.Component { componentDidMount() { this.props.children[1] =

; // Mutation is illegal @@ -82,27 +96,36 @@ describe('ReactComponent', () => { } if (__DEV__) { - expect(() => { - ReactTestUtils.renderIntoDocument( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render( + + + + + , + ); + }), + ).rejects.toThrowError(/Cannot assign to read only property.*/); + } else { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( , ); - }).toThrowError(/Cannot assign to read only property.*/); - } else { - ReactTestUtils.renderIntoDocument( - - - - - , - ); + }); } }); - it('should support string refs on owned components', () => { + it('should support string refs on owned components', async () => { const innerObj = {}; const outerObj = {}; @@ -133,8 +156,12 @@ describe('ReactComponent', () => { } } - expect(() => { - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev([ 'Warning: Component "div" contains the string ref "inner". ' + 'Support for string refs will be removed in a future major release. ' + @@ -151,7 +178,7 @@ describe('ReactComponent', () => { ]); }); - it('should not have string refs on unmounted components', () => { + it('should not have string refs on unmounted components', async () => { class Parent extends React.Component { render() { return ( @@ -172,10 +199,14 @@ describe('ReactComponent', () => { } } - ReactTestUtils.renderIntoDocument(} />); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(} />); + }); }); - it('should support callback-style refs', () => { + it('should support callback-style refs', async () => { const innerObj = {}; const outerObj = {}; @@ -211,11 +242,16 @@ describe('ReactComponent', () => { } } - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(mounted).toBe(true); }); - it('should support object-style refs', () => { + it('should support object-style refs', async () => { const innerObj = {}; const outerObj = {}; @@ -254,11 +290,16 @@ describe('ReactComponent', () => { } } - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(mounted).toBe(true); }); - it('should support new-style refs with mixed-up owners', () => { + it('should support new-style refs with mixed-up owners', async () => { class Wrapper extends React.Component { getTitle = () => { return this.props.title; @@ -296,11 +337,17 @@ describe('ReactComponent', () => { } } - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + expect(mounted).toBe(true); }); - it('should call refs at the correct time', () => { + it('should call refs at the correct time', async () => { const log = []; class Inner extends React.Component { @@ -358,11 +405,18 @@ describe('ReactComponent', () => { // mount, update, unmount const el = document.createElement('div'); log.push('start mount'); - ReactDOM.render(, el); + const root = ReactDOMClient.createRoot(el); + await act(() => { + root.render(); + }); log.push('start update'); - ReactDOM.render(, el); + await act(() => { + root.render(); + }); log.push('start unmount'); - ReactDOM.unmountComponentAtNode(el); + await act(() => { + root.unmount(); + }); /* eslint-disable indent */ expect(log).toEqual([ @@ -396,7 +450,7 @@ describe('ReactComponent', () => { /* eslint-enable indent */ }); - it('fires the callback after a component is rendered', () => { + it('fires the callback after a component is rendered in legacy roots', () => { const callback = jest.fn(); const container = document.createElement('div'); ReactDOM.render(

, container, callback); @@ -407,14 +461,20 @@ describe('ReactComponent', () => { expect(callback).toHaveBeenCalledTimes(3); }); - it('throws usefully when rendering badly-typed elements', () => { + it('throws usefully when rendering badly-typed elements', async () => { const X = undefined; - expect(() => { - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + let container = document.createElement('div'); + let root = ReactDOMClient.createRoot(container); + await expect( + expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'React.createElement: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.', - ); - }).toThrowError( + ), + ).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.' + (__DEV__ @@ -424,18 +484,24 @@ describe('ReactComponent', () => { ); const Y = null; - expect(() => { - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + container = document.createElement('div'); + root = ReactDOMClient.createRoot(container); + await expect( + expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'React.createElement: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: null.', - ); - }).toThrowError( + ), + ).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: null.', ); }); - it('includes owner name in the error about badly-typed elements', () => { + it('includes owner name in the error about badly-typed elements', async () => { const X = undefined; function Indirection(props) { @@ -454,12 +520,18 @@ describe('ReactComponent', () => { return ; } - expect(() => { - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect( + expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'React.createElement: type is invalid -- expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.', - ); - }).toThrowError( + ), + ).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.' + (__DEV__ @@ -470,7 +542,7 @@ describe('ReactComponent', () => { ); }); - it('throws if a plain object is used as a child', () => { + it('throws if a plain object is used as a child', async () => { const children = { x: , y: , @@ -478,15 +550,18 @@ describe('ReactComponent', () => { }; const element =
{[children]}
; const container = document.createElement('div'); - expect(() => { - ReactDOM.render(element, container); - }).toThrowError( + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render(element); + }), + ).rejects.toThrowError( 'Objects are not valid as a React child (found: object with keys {x, y, z}). ' + 'If you meant to render a collection of children, use an array instead.', ); }); - it('throws if a plain object even if it is in an owner', () => { + it('throws if a plain object even if it is in an owner', async () => { class Foo extends React.Component { render() { const children = { @@ -498,9 +573,12 @@ describe('ReactComponent', () => { } } const container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toThrowError( + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render(); + }), + ).rejects.toThrowError( 'Objects are not valid as a React child (found: object with keys {a, b, c}).' + ' If you meant to render a collection of children, use an array ' + 'instead.', @@ -545,12 +623,17 @@ describe('ReactComponent', () => { }); describe('with new features', () => { - it('warns on function as a return value from a function', () => { + it('warns on function as a return value from a function', async () => { function Foo() { return Foo; } const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + @@ -558,14 +641,20 @@ describe('ReactComponent', () => { ); }); - it('warns on function as a return value from a class', () => { + it('warns on function as a return value from a class', async () => { class Foo extends React.Component { render() { return Foo; } } const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + await expect(async () => { + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + @@ -573,7 +662,7 @@ describe('ReactComponent', () => { ); }); - it('warns on function as a child to host component', () => { + it('warns on function as a child to host component', async () => { function Foo() { return (
@@ -582,7 +671,12 @@ describe('ReactComponent', () => { ); } const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( 'Warning: Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + @@ -592,7 +686,7 @@ describe('ReactComponent', () => { ); }); - it('does not warn for function-as-a-child that gets resolved', () => { + it('does not warn for function-as-a-child that gets resolved', async () => { function Bar(props) { return props.children(); } @@ -600,11 +694,15 @@ describe('ReactComponent', () => { return {() => 'Hello'}; } const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('Hello'); }); - it('deduplicates function type warnings based on component type', () => { + it('deduplicates function type warnings based on component type', async () => { class Foo extends React.PureComponent { constructor() { super(); @@ -624,9 +722,12 @@ describe('ReactComponent', () => { } } const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); let component; - expect(() => { - component = ReactDOM.render(, container); + await expect(async () => { + await act(() => { + root.render( (component = current)} />); + }); }).toErrorDev([ 'Warning: Functions are not valid as a React child. This may happen if ' + 'you return a Component instead of from render. ' + @@ -640,7 +741,9 @@ describe('ReactComponent', () => { ' in div (at **)\n' + ' in Foo (at **)', ]); - component.setState({type: 'portobello mushrooms'}); + await act(() => { + component.setState({type: 'portobello mushrooms'}); + }); }); }); }); diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index ac13b3581775b..4567ee62a0a3e 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -113,9 +113,21 @@ describe('ReactComponentLifeCycle', () => { } const element = ; - const firstInstance = ReactDOM.render(element, container); - ReactDOM.unmountComponentAtNode(container); - const secondInstance = ReactDOM.render(element, container); + let root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(element); + }); + + const firstInstance = container.firstChild; + await act(() => { + root.unmount(); + }); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(element); + }); + + const secondInstance = container.firstChild; expect(firstInstance).not.toBe(secondInstance); }); @@ -123,7 +135,7 @@ describe('ReactComponentLifeCycle', () => { * If a state update triggers rerendering that in turn fires an onDOMReady, * that second onDOMReady should not fail. */ - it('it should fire onDOMReady when already in onDOMReady', () => { + it('it should fire onDOMReady when already in onDOMReady', async () => { const _testJournal = []; class Child extends React.Component { @@ -161,7 +173,13 @@ describe('ReactComponentLifeCycle', () => { } } - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + expect(_testJournal).toEqual([ 'SwitcherParent:getInitialState', 'SwitcherParent:onDOMReady', @@ -205,7 +223,7 @@ describe('ReactComponentLifeCycle', () => { }).not.toThrow(); }); - it("warns if setting 'this.state = props'", () => { + it("warns if setting 'this.state = props'", async () => { class StatefulComponent extends React.Component { constructor(props, context) { super(props, context); @@ -216,8 +234,12 @@ describe('ReactComponentLifeCycle', () => { } } - expect(() => { - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( 'StatefulComponent: It is not recommended to assign props directly to state ' + "because updates to props won't be reflected in state. " + @@ -225,7 +247,7 @@ describe('ReactComponentLifeCycle', () => { ); }); - it('should not allow update state inside of getInitialState', () => { + it('should not allow update state inside of getInitialState', async () => { class StatefulComponent extends React.Component { constructor(props, context) { super(props, context); @@ -239,8 +261,12 @@ describe('ReactComponentLifeCycle', () => { } } - expect(() => { - ReactTestUtils.renderIntoDocument(); + let container = document.createElement('div'); + let root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev( "Warning: Can't call setState on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + @@ -248,11 +274,14 @@ describe('ReactComponentLifeCycle', () => { 'class property with the desired state in the StatefulComponent component.', ); - // Check deduplication; (no extra warnings should be logged). - ReactTestUtils.renderIntoDocument(); + container = document.createElement('div'); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); }); - it('should correctly determine if a component is mounted', () => { + it('should correctly determine if a component is mounted', async () => { class Component extends React.Component { _isMounted() { // No longer a public API, but we can test that it works internally by @@ -271,15 +300,20 @@ describe('ReactComponentLifeCycle', () => { } } - const element = ; + let instance; + const element = (instance = current)} />; - expect(() => { - const instance = ReactTestUtils.renderIntoDocument(element); - expect(instance._isMounted()).toBeTruthy(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(element); + }); }).toErrorDev('Component is accessing isMounted inside its render()'); + expect(instance._isMounted()).toBeTruthy(); }); - it('should correctly determine if a null component is mounted', () => { + it('should correctly determine if a null component is mounted', async () => { class Component extends React.Component { _isMounted() { // No longer a public API, but we can test that it works internally by @@ -298,12 +332,17 @@ describe('ReactComponentLifeCycle', () => { } } - const element = ; + let instance; + const element = (instance = current)} />; - expect(() => { - const instance = ReactTestUtils.renderIntoDocument(element); - expect(instance._isMounted()).toBeTruthy(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(element); + }); }).toErrorDev('Component is accessing isMounted inside its render()'); + expect(instance._isMounted()).toBeTruthy(); }); it('isMounted should return false when unmounted', async () => { @@ -331,7 +370,7 @@ describe('ReactComponentLifeCycle', () => { expect(instance.updater.isMounted(instance)).toBe(false); }); - it('warns if findDOMNode is used inside render', () => { + it('warns if legacy findDOMNode is used inside render', async () => { class Component extends React.Component { state = {isMounted: false}; componentDidMount() { @@ -345,8 +384,12 @@ describe('ReactComponentLifeCycle', () => { } } - expect(() => { - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(() => { + root.render(); + }); }).toErrorDev('Component is accessing findDOMNode inside its render()'); }); diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponentDOMMinimalism-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponentDOMMinimalism-test.js index 7fc24cfbf2009..84de6278bad95 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponentDOMMinimalism-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponentDOMMinimalism-test.js @@ -11,15 +11,13 @@ // Requires let React; -let ReactDOM; -let ReactTestUtils; +let ReactDOMClient; +let act; // Test components let LowerLevelComposite; let MyCompositeComponent; -let expectSingleChildlessDiv; - /** * Integration test, testing the combination of JSX with our unit of * abstraction, `ReactCompositeComponent` does not ever add superfluous DOM @@ -28,8 +26,8 @@ let expectSingleChildlessDiv; describe('ReactCompositeComponentDOMMinimalism', () => { beforeEach(() => { React = require('react'); - ReactDOM = require('react-dom'); - ReactTestUtils = require('react-dom/test-utils'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; LowerLevelComposite = class extends React.Component { render() { @@ -42,39 +40,51 @@ describe('ReactCompositeComponentDOMMinimalism', () => { return {this.props.children}; } }; - - expectSingleChildlessDiv = function (instance) { - const el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('DIV'); - expect(el.children.length).toBe(0); - }; }); - it('should not render extra nodes for non-interpolated text', () => { - let instance = A string child; - instance = ReactTestUtils.renderIntoDocument(instance); - expectSingleChildlessDiv(instance); + it('should not render extra nodes for non-interpolated text', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(A string child); + }); + + const instance = container.firstChild; + expect(instance.tagName).toBe('DIV'); + expect(instance.children.length).toBe(0); }); - it('should not render extra nodes for non-interpolated text', () => { - let instance = ( - {'Interpolated String Child'} - ); - instance = ReactTestUtils.renderIntoDocument(instance); - expectSingleChildlessDiv(instance); + it('should not render extra nodes for non-interpolated text', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + + {'Interpolated String Child'} + , + ); + }); + + const instance = container.firstChild; + expect(instance.tagName).toBe('DIV'); + expect(instance.children.length).toBe(0); }); - it('should not render extra nodes for non-interpolated text', () => { - let instance = ( - -
    This text causes no children in ul, just innerHTML
-
- ); - instance = ReactTestUtils.renderIntoDocument(instance); - const el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('DIV'); - expect(el.children.length).toBe(1); - expect(el.children[0].tagName).toBe('UL'); - expect(el.children[0].children.length).toBe(0); + it('should not render extra nodes for non-interpolated text', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + +
    This text causes no children in ul, just innerHTML
+
, + ); + }); + + const instance = container.firstChild; + expect(instance.tagName).toBe('DIV'); + expect(instance.children.length).toBe(1); + expect(instance.children[0].tagName).toBe('UL'); + expect(instance.children[0].children.length).toBe(0); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js index eb093d389b629..b90ca9efdb32e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js @@ -9,45 +9,55 @@ describe('ReactDOMEventListener', () => { let React; - let OuterReactDOM; + let OuterReactDOMClient; let InnerReactDOM; + let InnerReactDOMClient; + let act; let container; + let root; beforeEach(() => { window.TextEvent = function () {}; jest.resetModules(); jest.isolateModules(() => { React = require('react'); - OuterReactDOM = require('react-dom'); + act = require('internal-test-utils').act; + OuterReactDOMClient = require('react-dom/client'); }); jest.isolateModules(() => { InnerReactDOM = require('react-dom'); + InnerReactDOMClient = require('react-dom/client'); }); - expect(OuterReactDOM).not.toBe(InnerReactDOM); + expect(OuterReactDOMClient).not.toBe(InnerReactDOMClient); }); - afterEach(() => { - cleanup(); + afterEach(async () => { + await cleanup(); }); - function cleanup() { + async function cleanup() { if (container) { - OuterReactDOM.unmountComponentAtNode(container); + await act(() => { + root.unmount(); + }); document.body.removeChild(container); container = null; } } - function render(tree) { - cleanup(); + async function render(tree) { + await cleanup(); container = document.createElement('div'); document.body.appendChild(container); - OuterReactDOM.render(tree, container); + root = OuterReactDOMClient.createRoot(container); + await act(() => { + root.render(tree); + }); } describe('bubbling events', () => { - it('onAnimationEnd', () => { - testNativeBubblingEvent({ + it('onAnimationEnd', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onAnimationEnd', reactEventType: 'animationend', @@ -63,8 +73,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onAnimationIteration', () => { - testNativeBubblingEvent({ + it('onAnimationIteration', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onAnimationIteration', reactEventType: 'animationiteration', @@ -80,8 +90,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onAnimationStart', () => { - testNativeBubblingEvent({ + it('onAnimationStart', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onAnimationStart', reactEventType: 'animationstart', @@ -97,8 +107,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onAuxClick', () => { - testNativeBubblingEvent({ + it('onAuxClick', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onAuxClick', reactEventType: 'auxclick', @@ -114,8 +124,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onBlur', () => { - testNativeBubblingEvent({ + it('onBlur', async () => { + await testNativeBubblingEvent({ type: 'input', reactEvent: 'onBlur', reactEventType: 'blur', @@ -134,8 +144,8 @@ describe('ReactDOMEventListener', () => { // because we emulate the React 16 behavior where // the click handler is attached to the document. // @gate !enableLegacyFBSupport - it('onClick', () => { - testNativeBubblingEvent({ + it('onClick', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onClick', reactEventType: 'click', @@ -146,8 +156,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onContextMenu', () => { - testNativeBubblingEvent({ + it('onContextMenu', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onContextMenu', reactEventType: 'contextmenu', @@ -163,8 +173,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onCopy', () => { - testNativeBubblingEvent({ + it('onCopy', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onCopy', reactEventType: 'copy', @@ -180,8 +190,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onCut', () => { - testNativeBubblingEvent({ + it('onCut', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onCut', reactEventType: 'cut', @@ -197,8 +207,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDoubleClick', () => { - testNativeBubblingEvent({ + it('onDoubleClick', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDoubleClick', reactEventType: 'dblclick', @@ -214,8 +224,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDrag', () => { - testNativeBubblingEvent({ + it('onDrag', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDrag', reactEventType: 'drag', @@ -231,8 +241,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragEnd', () => { - testNativeBubblingEvent({ + it('onDragEnd', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragEnd', reactEventType: 'dragend', @@ -248,8 +258,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragEnter', () => { - testNativeBubblingEvent({ + it('onDragEnter', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragEnter', reactEventType: 'dragenter', @@ -265,8 +275,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragExit', () => { - testNativeBubblingEvent({ + it('onDragExit', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragExit', reactEventType: 'dragexit', @@ -282,8 +292,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragLeave', () => { - testNativeBubblingEvent({ + it('onDragLeave', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragLeave', reactEventType: 'dragleave', @@ -299,8 +309,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragOver', () => { - testNativeBubblingEvent({ + it('onDragOver', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragOver', reactEventType: 'dragover', @@ -316,8 +326,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDragStart', () => { - testNativeBubblingEvent({ + it('onDragStart', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDragStart', reactEventType: 'dragstart', @@ -333,8 +343,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDrop', () => { - testNativeBubblingEvent({ + it('onDrop', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onDrop', reactEventType: 'drop', @@ -350,8 +360,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onFocus', () => { - testNativeBubblingEvent({ + it('onFocus', async () => { + await testNativeBubblingEvent({ type: 'input', reactEvent: 'onFocus', reactEventType: 'focus', @@ -366,8 +376,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onGotPointerCapture', () => { - testNativeBubblingEvent({ + it('onGotPointerCapture', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onGotPointerCapture', reactEventType: 'gotpointercapture', @@ -383,8 +393,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onKeyDown', () => { - testNativeBubblingEvent({ + it('onKeyDown', async () => { + await testNativeBubblingEvent({ type: 'input', reactEvent: 'onKeyDown', reactEventType: 'keydown', @@ -400,8 +410,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onKeyPress', () => { - testNativeBubblingEvent({ + it('onKeyPress', async () => { + await testNativeBubblingEvent({ type: 'input', reactEvent: 'onKeyPress', reactEventType: 'keypress', @@ -418,8 +428,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onKeyUp', () => { - testNativeBubblingEvent({ + it('onKeyUp', async () => { + await testNativeBubblingEvent({ type: 'input', reactEvent: 'onKeyUp', reactEventType: 'keyup', @@ -435,8 +445,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onLostPointerCapture', () => { - testNativeBubblingEvent({ + it('onLostPointerCapture', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onLostPointerCapture', reactEventType: 'lostpointercapture', @@ -452,8 +462,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onMouseDown', () => { - testNativeBubblingEvent({ + it('onMouseDown', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onMouseDown', reactEventType: 'mousedown', @@ -469,8 +479,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onMouseOut', () => { - testNativeBubblingEvent({ + it('onMouseOut', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onMouseOut', reactEventType: 'mouseout', @@ -486,8 +496,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onMouseOver', () => { - testNativeBubblingEvent({ + it('onMouseOver', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onMouseOver', reactEventType: 'mouseover', @@ -503,8 +513,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onMouseUp', () => { - testNativeBubblingEvent({ + it('onMouseUp', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onMouseUp', reactEventType: 'mouseup', @@ -520,8 +530,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPaste', () => { - testNativeBubblingEvent({ + it('onPaste', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPaste', reactEventType: 'paste', @@ -537,8 +547,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerCancel', () => { - testNativeBubblingEvent({ + it('onPointerCancel', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerCancel', reactEventType: 'pointercancel', @@ -554,8 +564,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerDown', () => { - testNativeBubblingEvent({ + it('onPointerDown', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerDown', reactEventType: 'pointerdown', @@ -571,8 +581,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerMove', () => { - testNativeBubblingEvent({ + it('onPointerMove', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerMove', reactEventType: 'pointermove', @@ -588,8 +598,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerOut', () => { - testNativeBubblingEvent({ + it('onPointerOut', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerOut', reactEventType: 'pointerout', @@ -605,8 +615,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerOver', () => { - testNativeBubblingEvent({ + it('onPointerOver', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerOver', reactEventType: 'pointerover', @@ -622,8 +632,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPointerUp', () => { - testNativeBubblingEvent({ + it('onPointerUp', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onPointerUp', reactEventType: 'pointerup', @@ -639,8 +649,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onReset', () => { - testNativeBubblingEvent({ + it('onReset', async () => { + await testNativeBubblingEvent({ type: 'form', reactEvent: 'onReset', reactEventType: 'reset', @@ -655,8 +665,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onSubmit', () => { - testNativeBubblingEvent({ + it('onSubmit', async () => { + await testNativeBubblingEvent({ type: 'form', reactEvent: 'onSubmit', reactEventType: 'submit', @@ -671,8 +681,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTouchCancel', () => { - testNativeBubblingEvent({ + it('onTouchCancel', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onTouchCancel', reactEventType: 'touchcancel', @@ -688,8 +698,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTouchEnd', () => { - testNativeBubblingEvent({ + it('onTouchEnd', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onTouchEnd', reactEventType: 'touchend', @@ -705,8 +715,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTouchMove', () => { - testNativeBubblingEvent({ + it('onTouchMove', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onTouchMove', reactEventType: 'touchmove', @@ -722,8 +732,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTouchStart', () => { - testNativeBubblingEvent({ + it('onTouchStart', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onTouchStart', reactEventType: 'touchstart', @@ -739,8 +749,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTransitionEnd', () => { - testNativeBubblingEvent({ + it('onTransitionEnd', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onTransitionEnd', reactEventType: 'transitionend', @@ -756,8 +766,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onWheel', () => { - testNativeBubblingEvent({ + it('onWheel', async () => { + await testNativeBubblingEvent({ type: 'div', reactEvent: 'onWheel', reactEventType: 'wheel', @@ -775,8 +785,8 @@ describe('ReactDOMEventListener', () => { }); describe('non-bubbling events that bubble in React', () => { - it('onAbort', () => { - testEmulatedBubblingEvent({ + it('onAbort', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onAbort', reactEventType: 'abort', @@ -791,8 +801,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onCancel', () => { - testEmulatedBubblingEvent({ + it('onCancel', async () => { + await testEmulatedBubblingEvent({ type: 'dialog', reactEvent: 'onCancel', reactEventType: 'cancel', @@ -807,8 +817,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onCanPlay', () => { - testEmulatedBubblingEvent({ + it('onCanPlay', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onCanPlay', reactEventType: 'canplay', @@ -823,8 +833,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onCanPlayThrough', () => { - testEmulatedBubblingEvent({ + it('onCanPlayThrough', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onCanPlayThrough', reactEventType: 'canplaythrough', @@ -839,8 +849,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onClose', () => { - testEmulatedBubblingEvent({ + it('onClose', async () => { + await testEmulatedBubblingEvent({ type: 'dialog', reactEvent: 'onClose', reactEventType: 'close', @@ -855,8 +865,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onDurationChange', () => { - testEmulatedBubblingEvent({ + it('onDurationChange', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onDurationChange', reactEventType: 'durationchange', @@ -871,8 +881,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onEmptied', () => { - testEmulatedBubblingEvent({ + it('onEmptied', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onEmptied', reactEventType: 'emptied', @@ -887,8 +897,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onEncrypted', () => { - testEmulatedBubblingEvent({ + it('onEncrypted', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onEncrypted', reactEventType: 'encrypted', @@ -903,8 +913,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onEnded', () => { - testEmulatedBubblingEvent({ + it('onEnded', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onEnded', reactEventType: 'ended', @@ -919,8 +929,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onError', () => { - testEmulatedBubblingEvent({ + it('onError', async () => { + await testEmulatedBubblingEvent({ type: 'img', reactEvent: 'onError', reactEventType: 'error', @@ -935,8 +945,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onInvalid', () => { - testEmulatedBubblingEvent({ + it('onInvalid', async () => { + await testEmulatedBubblingEvent({ type: 'input', reactEvent: 'onInvalid', reactEventType: 'invalid', @@ -951,8 +961,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onLoad', () => { - testEmulatedBubblingEvent({ + it('onLoad', async () => { + await testEmulatedBubblingEvent({ type: 'img', reactEvent: 'onLoad', reactEventType: 'load', @@ -967,8 +977,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onLoadedData', () => { - testEmulatedBubblingEvent({ + it('onLoadedData', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onLoadedData', reactEventType: 'loadeddata', @@ -983,8 +993,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onLoadedMetadata', () => { - testEmulatedBubblingEvent({ + it('onLoadedMetadata', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onLoadedMetadata', reactEventType: 'loadedmetadata', @@ -999,8 +1009,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onLoadStart', () => { - testEmulatedBubblingEvent({ + it('onLoadStart', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onLoadStart', reactEventType: 'loadstart', @@ -1015,8 +1025,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPause', () => { - testEmulatedBubblingEvent({ + it('onPause', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onPause', reactEventType: 'pause', @@ -1031,8 +1041,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPlay', () => { - testEmulatedBubblingEvent({ + it('onPlay', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onPlay', reactEventType: 'play', @@ -1047,8 +1057,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onPlaying', () => { - testEmulatedBubblingEvent({ + it('onPlaying', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onPlaying', reactEventType: 'playing', @@ -1063,8 +1073,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onProgress', () => { - testEmulatedBubblingEvent({ + it('onProgress', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onProgress', reactEventType: 'progress', @@ -1079,8 +1089,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onRateChange', () => { - testEmulatedBubblingEvent({ + it('onRateChange', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onRateChange', reactEventType: 'ratechange', @@ -1095,8 +1105,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onResize', () => { - testEmulatedBubblingEvent({ + it('onResize', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onResize', reactEventType: 'resize', @@ -1111,8 +1121,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onSeeked', () => { - testEmulatedBubblingEvent({ + it('onSeeked', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onSeeked', reactEventType: 'seeked', @@ -1127,8 +1137,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onSeeking', () => { - testEmulatedBubblingEvent({ + it('onSeeking', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onSeeking', reactEventType: 'seeking', @@ -1143,8 +1153,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onStalled', () => { - testEmulatedBubblingEvent({ + it('onStalled', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onStalled', reactEventType: 'stalled', @@ -1159,8 +1169,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onSuspend', () => { - testEmulatedBubblingEvent({ + it('onSuspend', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onSuspend', reactEventType: 'suspend', @@ -1175,8 +1185,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onTimeUpdate', () => { - testEmulatedBubblingEvent({ + it('onTimeUpdate', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onTimeUpdate', reactEventType: 'timeupdate', @@ -1191,8 +1201,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onToggle', () => { - testEmulatedBubblingEvent({ + it('onToggle', async () => { + await testEmulatedBubblingEvent({ type: 'details', reactEvent: 'onToggle', reactEventType: 'toggle', @@ -1207,8 +1217,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onVolumeChange', () => { - testEmulatedBubblingEvent({ + it('onVolumeChange', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onVolumeChange', reactEventType: 'volumechange', @@ -1223,8 +1233,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onWaiting', () => { - testEmulatedBubblingEvent({ + it('onWaiting', async () => { + await testEmulatedBubblingEvent({ type: 'video', reactEvent: 'onWaiting', reactEventType: 'waiting', @@ -1241,8 +1251,8 @@ describe('ReactDOMEventListener', () => { }); describe('non-bubbling events that do not bubble in React', () => { - it('onScroll', () => { - testNonBubblingEvent({ + it('onScroll', async () => { + await testNonBubblingEvent({ type: 'div', reactEvent: 'onScroll', reactEventType: 'scroll', @@ -1257,8 +1267,8 @@ describe('ReactDOMEventListener', () => { }); }); - it('onScrollEnd', () => { - testNonBubblingEvent({ + it('onScrollEnd', async () => { + await testNonBubblingEvent({ type: 'div', reactEvent: 'onScrollEnd', reactEventType: 'scrollend', @@ -1279,10 +1289,10 @@ describe('ReactDOMEventListener', () => { // work very well across different roots. For now, we'll // just document the current state in these tests. describe('enter/leave events', () => { - it('onMouseEnter and onMouseLeave', () => { + it('onMouseEnter and onMouseLeave', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onPointerEnter and onPointerLeave', () => { + it('onPointerEnter and onPointerLeave', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { // work very well across different roots. For now, we'll // just document the current state in these tests. describe('polyfilled events', () => { - it('onBeforeInput', () => { + it('onBeforeInput', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onChange', () => { + it('onChange', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onCompositionStart', () => { + it('onCompositionStart', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onCompositionEnd', () => { + it('onCompositionEnd', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onCompositionUpdate', () => { + it('onCompositionUpdate', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { `); }); - it('onSelect', () => { + it('onSelect', async () => { const log = []; const targetRef = React.createRef(); - render( + await render( { // Events that bubble in React and in the browser. // React delegates them to the root. - function testNativeBubblingEvent(config) { - testNativeBubblingEventWithTargetListener(config); - testNativeBubblingEventWithoutTargetListener(config); - testReactStopPropagationInOuterCapturePhase(config); - testReactStopPropagationInInnerCapturePhase(config); - testReactStopPropagationInInnerBubblePhase(config); - testReactStopPropagationInOuterBubblePhase(config); - testNativeStopPropagationInOuterCapturePhase(config); - testNativeStopPropagationInInnerCapturePhase(config); - testNativeStopPropagationInInnerBubblePhase(config); - testNativeStopPropagationInOuterBubblePhase(config); + async function testNativeBubblingEvent(config) { + await testNativeBubblingEventWithTargetListener(config); + await testNativeBubblingEventWithoutTargetListener(config); + await testReactStopPropagationInOuterCapturePhase(config); + await testReactStopPropagationInInnerCapturePhase(config); + await testReactStopPropagationInInnerBubblePhase(config); + await testReactStopPropagationInOuterBubblePhase(config); + await testNativeStopPropagationInOuterCapturePhase(config); + await testNativeStopPropagationInInnerCapturePhase(config); + await testNativeStopPropagationInInnerBubblePhase(config); + await testNativeStopPropagationInOuterBubblePhase(config); } // Events that bubble in React but not in the browser. // React attaches them to the elements. - function testEmulatedBubblingEvent(config) { - testEmulatedBubblingEventWithTargetListener(config); - testEmulatedBubblingEventWithoutTargetListener(config); - testReactStopPropagationInOuterCapturePhase(config); - testReactStopPropagationInInnerCapturePhase(config); - testReactStopPropagationInInnerBubblePhase(config); - testNativeStopPropagationInOuterCapturePhase(config); - testNativeStopPropagationInInnerCapturePhase(config); - testNativeStopPropagationInInnerEmulatedBubblePhase(config); + async function testEmulatedBubblingEvent(config) { + await testEmulatedBubblingEventWithTargetListener(config); + await testEmulatedBubblingEventWithoutTargetListener(config); + await testReactStopPropagationInOuterCapturePhase(config); + await testReactStopPropagationInInnerCapturePhase(config); + await testReactStopPropagationInInnerBubblePhase(config); + await testNativeStopPropagationInOuterCapturePhase(config); + await testNativeStopPropagationInInnerCapturePhase(config); + await testNativeStopPropagationInInnerEmulatedBubblePhase(config); } // Events that don't bubble either in React or in the browser. - function testNonBubblingEvent(config) { - testNonBubblingEventWithTargetListener(config); - testNonBubblingEventWithoutTargetListener(config); - testReactStopPropagationInOuterCapturePhase(config); - testReactStopPropagationInInnerCapturePhase(config); - testReactStopPropagationInInnerBubblePhase(config); - testNativeStopPropagationInOuterCapturePhase(config); - testNativeStopPropagationInInnerCapturePhase(config); + async function testNonBubblingEvent(config) { + await testNonBubblingEventWithTargetListener(config); + await testNonBubblingEventWithoutTargetListener(config); + await testReactStopPropagationInOuterCapturePhase(config); + await testReactStopPropagationInInnerCapturePhase(config); + await testReactStopPropagationInInnerBubblePhase(config); + await testNativeStopPropagationInOuterCapturePhase(config); + await testNativeStopPropagationInInnerCapturePhase(config); } - function testNativeBubblingEventWithTargetListener(eventConfig) { + async function testNativeBubblingEventWithTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testEmulatedBubblingEventWithTargetListener(eventConfig) { + async function testEmulatedBubblingEventWithTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNonBubblingEventWithTargetListener(eventConfig) { + async function testNonBubblingEventWithTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNativeBubblingEventWithoutTargetListener(eventConfig) { + async function testNativeBubblingEventWithoutTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testEmulatedBubblingEventWithoutTargetListener(eventConfig) { + async function testEmulatedBubblingEventWithoutTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNonBubblingEventWithoutTargetListener(eventConfig) { + async function testNonBubblingEventWithoutTargetListener(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testReactStopPropagationInOuterCapturePhase(eventConfig) { + async function testReactStopPropagationInOuterCapturePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { @@ -2235,10 +2245,10 @@ describe('ReactDOMEventListener', () => { `); } - function testReactStopPropagationInInnerCapturePhase(eventConfig) { + async function testReactStopPropagationInInnerCapturePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { @@ -2299,10 +2309,10 @@ describe('ReactDOMEventListener', () => { `); } - function testReactStopPropagationInInnerBubblePhase(eventConfig) { + async function testReactStopPropagationInInnerBubblePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testReactStopPropagationInOuterBubblePhase(eventConfig) { + async function testReactStopPropagationInOuterBubblePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNativeStopPropagationInOuterCapturePhase(eventConfig) { + async function testNativeStopPropagationInOuterCapturePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNativeStopPropagationInInnerCapturePhase(eventConfig) { + async function testNativeStopPropagationInInnerCapturePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { `); } - function testNativeStopPropagationInInnerBubblePhase(eventConfig) { + async function testNativeStopPropagationInInnerBubblePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { @@ -2625,10 +2635,12 @@ describe('ReactDOMEventListener', () => { `); } - function testNativeStopPropagationInInnerEmulatedBubblePhase(eventConfig) { + async function testNativeStopPropagationInInnerEmulatedBubblePhase( + eventConfig, + ) { const log = []; const targetRef = React.createRef(); - render( + await render( { @@ -2694,10 +2706,10 @@ describe('ReactDOMEventListener', () => { `); } - function testNativeStopPropagationInOuterBubblePhase(eventConfig) { + async function testNativeStopPropagationInOuterBubblePhase(eventConfig) { const log = []; const targetRef = React.createRef(); - render( + await render( { const parent = ref.current; const innerContainer = document.createElement('div'); parent.appendChild(innerContainer); - InnerReactDOM.render(children, innerContainer); + const innerReactRoot = InnerReactDOMClient.createRoot(innerContainer); + InnerReactDOM.flushSync(() => { + innerReactRoot.render(children); + }); return () => { - InnerReactDOM.unmountComponentAtNode(innerContainer); + innerReactRoot.unmount(); parent.removeChild(innerContainer); }; }, [children, ref]); diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index 0b6d5dabffe42..76a8229e5a89f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -25,7 +25,7 @@ describe('ReactDOMServerHydration', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - act = React.unstable_act; + act = React.act; console.error = jest.fn(); container = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactDOMIframe-test.js b/packages/react-dom/src/__tests__/ReactDOMIframe-test.js index 2f3f03ad3df89..5ce6811b247b5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMIframe-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMIframe-test.js @@ -11,22 +11,30 @@ describe('ReactDOMIframe', () => { let React; - let ReactTestUtils; + let ReactDOMClient; + let act; beforeEach(() => { React = require('react'); - ReactTestUtils = require('react-dom/test-utils'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; }); - it('should trigger load events', () => { + it('should trigger load events', async () => { const onLoadSpy = jest.fn(); - let iframe = React.createElement('iframe', {onLoad: onLoadSpy}); - iframe = ReactTestUtils.renderIntoDocument(iframe); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(React.createElement('iframe', {onLoad: onLoadSpy})); + }); + const iframe = container.firstChild; const loadEvent = document.createEvent('Event'); loadEvent.initEvent('load', false, false); - iframe.dispatchEvent(loadEvent); + await act(() => { + iframe.dispatchEvent(loadEvent); + }); expect(onLoadSpy).toHaveBeenCalled(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js index 251e98d67533d..9457137ff4f5a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js @@ -10,16 +10,18 @@ 'use strict'; let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMSelection; +let act; let getModernOffsetsFromPoints; describe('ReactDOMSelection', () => { beforeEach(() => { React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMSelection = require('react-dom-bindings/src/client/ReactDOMSelection'); + act = require('internal-test-utils').act; ({getModernOffsetsFromPoints} = ReactDOMSelection); }); @@ -74,53 +76,57 @@ describe('ReactDOMSelection', () => { // Complicated example derived from a real-world DOM tree. Has a bit of // everything. - function getFixture() { - return ReactDOM.render( -
+ async function getFixture() { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(
-
-
xxxxxxxxxxxxxxxxxxxx
-
- x
- x +
xxxxxxxxxxxxxxxxxxxx
+
+ x +
+ x
-
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-
-
-
xxxxxxxxxxxxxxxxxx
+
+
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+
+
xxxxxxxxxxxxxxxxxx
+
+
-
-
-
-
xxxx
-
xxxxxxxxxxxxxxxxxxx
+
+
xxxx
+
xxxxxxxxxxxxxxxxxxx
+
-
-
xxx
-
xxxxx
-
xxx
-
+
xxx
+
xxxxx
+
xxx
-
{['x', 'x', 'xxx']}
+
+
{['x', 'x', 'xxx']}
+
-
-
-
xxxxxx
-
-
, - document.createElement('div'), - ); +
+
xxxxxx
+
+
, + ); + }); + return container.firstChild; } it('returns correctly for base case', () => { @@ -135,8 +141,8 @@ describe('ReactDOMSelection', () => { }); }); - it('returns correctly for fuzz test', () => { - const fixtureRoot = getFixture(); + it('returns correctly for fuzz test', async () => { + const fixtureRoot = await getFixture(); const allNodes = [fixtureRoot].concat( Array.from(fixtureRoot.querySelectorAll('*')), ); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js index 36c3c0cb94840..bda021b5e06a7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js @@ -15,6 +15,7 @@ const ReactFeatureFlags = require('shared/ReactFeatureFlags'); let React; let ReactDOM; +let ReactDOMClient; let ReactTestUtils; let ReactDOMServer; @@ -23,12 +24,13 @@ function initModules() { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; @@ -638,7 +640,12 @@ describe('ReactDOMServerIntegration', () => { // DOM nodes on the client side. We force it to fire early // so that it gets deduplicated later, and doesn't fail the test. expect(() => { - ReactDOM.render(, document.createElement('div')); + ReactDOM.flushSync(() => { + const root = ReactDOMClient.createRoot( + document.createElement('div'), + ); + root.render(); + }); }).toErrorDev('The tag is unrecognized in this browser.'); const e = await render(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js index a0793a6215b66..2821dd088bb48 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js @@ -15,7 +15,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio const TEXT_NODE_TYPE = 3; let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -23,13 +23,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; @@ -149,8 +149,10 @@ describe('ReactDOMServerIntegration', () => { expect(await render([])).toBe(null); expect(await render(false)).toBe(null); expect(await render(true)).toBe(null); - expect(await render(undefined)).toBe(null); expect(await render([[[false]], undefined])).toBe(null); + + // hydrateRoot errors for undefined children. + expect(await render(undefined, 1)).toBe(null); }); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationCheckbox-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationCheckbox-test.js index 606ec88b89893..178ed7982a44f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationCheckbox-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationCheckbox-test.js @@ -15,7 +15,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -23,13 +23,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js index dda475b7ced3e..2df2d66b9b9fa 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationClassContextType-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js index c482010d39647..3bbdd379813e1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js @@ -16,19 +16,23 @@ const TEXT_NODE_TYPE = 3; let React; let ReactDOM; +let ReactDOMClient; let ReactDOMServer; +let ReactFeatureFlags; let ReactTestUtils; function initModules() { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; @@ -136,7 +140,13 @@ describe('ReactDOMServerIntegration', () => { // DOM nodes on the client side. We force it to fire early // so that it gets deduplicated later, and doesn't fail the test. expect(() => { - ReactDOM.render(, document.createElement('div')); + ReactDOM.flushSync(() => { + const root = ReactDOMClient.createRoot( + document.createElement('div'), + ); + + root.render(); + }); }).toErrorDev('The tag is unrecognized in this browser.'); const e = await render(Text); @@ -833,15 +843,21 @@ describe('ReactDOMServerIntegration', () => { 'an element with one text child with special characters', async render => { const e = await render(
{'foo\rbar\r\nbaz\nqux\u0000'}
); - if (render === serverRender || render === streamRender) { + if ( + render === serverRender || + render === streamRender || + (render === clientRenderOnServerString && + ReactFeatureFlags.enableClientRenderFallbackOnTextMismatch) + ) { expect(e.childNodes.length).toBe(1); - // Everything becomes LF when parsed from server HTML. + // Everything becomes LF when parsed from server HTML or hydrated if enableClientRenderFallbackOnTextMismatch is on. // Null character is ignored. expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux'); } else { expect(e.childNodes.length).toBe(1); - // Client rendering (or hydration) uses JS value with CR. + // Client rendering (or hydration without enableClientRenderFallbackOnTextMismatch) uses JS value with CR. // Null character stays. + expectNode( e.childNodes[0], TEXT_NODE_TYPE, @@ -860,17 +876,23 @@ describe('ReactDOMServerIntegration', () => { {'\r\nbaz\nqux\u0000'}
, ); - if (render === serverRender || render === streamRender) { + if ( + render === serverRender || + render === streamRender || + (render === clientRenderOnServerString && + ReactFeatureFlags.enableClientRenderFallbackOnTextMismatch) + ) { // We have three nodes because there is a comment between them. expect(e.childNodes.length).toBe(3); - // Everything becomes LF when parsed from server HTML. + // Everything becomes LF when parsed from server HTML or hydrated if enableClientRenderFallbackOnTextMismatch is on. // Null character is ignored. expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar'); expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux'); } else if (render === clientRenderOnServerString) { // We have three nodes because there is a comment between them. expect(e.childNodes.length).toBe(3); - // Hydration uses JS value with CR and null character. + // Hydration without enableClientRenderFallbackOnTextMismatch uses JS value with CR and null character. + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar'); expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000'); } else { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.js index 680f283b6dbf2..8e8fc2aa8fe27 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js index 08f0b1a8a7de8..7e46bea5f93d9 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js @@ -15,7 +15,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; let useState; @@ -39,7 +39,7 @@ function initModules() { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); useState = React.useState; @@ -67,14 +67,19 @@ function initModules() { // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; } -const {resetModules, itRenders, itThrowsWhenRendering, serverRender} = - ReactDOMServerIntegrationUtils(initModules); +const { + resetModules, + itRenders, + itThrowsWhenRendering, + clientRenderOnBadMarkup, + serverRender, +} = ReactDOMServerIntegrationUtils(initModules); describe('ReactDOMServerHooks', () => { beforeEach(() => { @@ -422,8 +427,13 @@ describe('ReactDOMServerHooks', () => { }); return 'hi'; } - - const domNode = await render(, 1); + const domNode = await render( + , + render === clientRenderOnBadMarkup + ? // On hydration mismatch we retry and therefore log the warning again. + 2 + : 1, + ); expect(domNode.textContent).toEqual('hi'); }); @@ -436,7 +446,13 @@ describe('ReactDOMServerHooks', () => { return value; } - const domNode = await render(, 1); + const domNode = await render( + , + render === clientRenderOnBadMarkup + ? // On hydration mismatch we retry and therefore log the warning again. + 2 + : 1, + ); expect(domNode.textContent).toEqual('0'); }); }); @@ -859,7 +875,13 @@ describe('ReactDOMServerHooks', () => { return ; } - const domNode1 = await render(, 1); + const domNode1 = await render( + , + render === clientRenderOnBadMarkup + ? // On hydration mismatch we retry and therefore log the warning again. + 2 + : 1, + ); expect(domNode1.textContent).toEqual('42'); const domNode2 = await render(, 1); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js index afbbf28a41ecb..54780dae52cdb 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationInput-test.js @@ -15,7 +15,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -23,13 +23,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js index e551a72b9ace4..99cf33b821f17 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js index cfcfc323e1d3d..cf0167eef1fd2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationNewContext-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,19 +21,20 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; } -const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); +const {resetModules, itRenders, clientRenderOnBadMarkup} = + ReactDOMServerIntegrationUtils(initModules); describe('ReactDOMServerIntegration', () => { beforeEach(() => { @@ -365,8 +366,13 @@ describe('ReactDOMServerIntegration', () => {
); }; - // We expect 1 error. - await render(, 1); + await render( + , + render === clientRenderOnBadMarkup + ? // On hydration mismatch we retry and therefore log the warning again. + 2 + : 1, + ); }, ); @@ -391,8 +397,14 @@ describe('ReactDOMServerIntegration', () => {
); }; - // We expect 1 error. - await render(, 1); + + await render( + , + render === clientRenderOnBadMarkup + ? // On hydration mismatch we retry and therefore log the warning again. + 2 + : 1, + ); }, ); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js index eb201c17b484d..76612f510d7d2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js @@ -13,30 +13,30 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio let React; let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; -function initModules() { - // Reset warning cache. - jest.resetModules(); - - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMServer = require('react-dom/server'); - ReactTestUtils = require('react-dom/test-utils'); - - // Make them available to the helpers. - return { - ReactDOM, - ReactDOMServer, - ReactTestUtils, - }; -} - -const {resetModules, expectMarkupMismatch, expectMarkupMatch} = - ReactDOMServerIntegrationUtils(initModules); - describe('ReactDOMServerIntegration', () => { + function initModules() { + // Reset warning cache. + jest.resetModules(); + + React = require('react'); + ReactDOMClient = require('react-dom/client'); + ReactDOMServer = require('react-dom/server'); + ReactTestUtils = require('react-dom/test-utils'); + + // Make them available to the helpers. + return { + ReactDOMClient, + ReactDOMServer, + ReactTestUtils, + }; + } + + const {resetModules, expectMarkupMismatch, expectMarkupMatch} = + ReactDOMServerIntegrationUtils(initModules); beforeEach(() => { resetModules(); }); @@ -123,8 +123,8 @@ describe('ReactDOMServerIntegration', () => { it('should error reconnecting different attribute values', () => expectMarkupMismatch(
,
)); - it('can explicitly ignore errors reconnecting different element types of children', () => - expectMarkupMatch( + it('should error reconnecting different element types of children', () => + expectMarkupMismatch(
, @@ -354,8 +354,8 @@ describe('ReactDOMServerIntegration', () => {
{''}
, )); - it('can explicitly ignore reconnecting more children', () => - expectMarkupMatch( + it('can not ignore reconnecting more children', () => + expectMarkupMismatch(
, @@ -365,8 +365,8 @@ describe('ReactDOMServerIntegration', () => {
, )); - it('can explicitly ignore reconnecting fewer children', () => - expectMarkupMatch( + it('can not ignore reconnecting fewer children', () => + expectMarkupMismatch(
@@ -376,8 +376,8 @@ describe('ReactDOMServerIntegration', () => {
, )); - it('can explicitly ignore reconnecting reordered children', () => - expectMarkupMatch( + it('can not ignore reconnecting reordered children', () => + expectMarkupMismatch(
@@ -456,3 +456,73 @@ describe('ReactDOMServerIntegration', () => { )); }); }); + +describe('ReactDOMServerIntegration (legacy)', () => { + function initModules() { + // Reset warning cache. + jest.resetModules(); + + React = require('react'); + ReactDOM = require('react-dom'); + ReactDOMServer = require('react-dom/server'); + ReactTestUtils = require('react-dom/test-utils'); + + // Make them available to the helpers. + return { + ReactDOM, + ReactDOMServer, + ReactTestUtils, + }; + } + + const {resetModules, expectMarkupMatch} = + ReactDOMServerIntegrationUtils(initModules); + + beforeEach(() => { + resetModules(); + }); + + it('legacy mode can explicitly ignore errors reconnecting different element types of children', () => + expectMarkupMatch( +
+
+
, +
+ +
, + )); + + it('legacy mode can explicitly ignore reconnecting more children', () => + expectMarkupMatch( +
+
+
, +
+
+
+
, + )); + + it('legacy mode can explicitly ignore reconnecting fewer children', () => + expectMarkupMatch( +
+
+
+
, +
+
+
, + )); + + it('legacy mode can explicitly ignore reconnecting reordered children', () => + expectMarkupMatch( +
+
+ +
, +
+ +
+
, + )); +}); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js index 76da3e92c82ad..e5564d3d9348c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js @@ -12,7 +12,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -20,13 +20,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js index e567ac9eac3ad..9e503be7520b2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSelect-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; @@ -253,7 +253,7 @@ describe('ReactDOMServerIntegrationSelect', () => { , - 1, + 2, ); expect(e.firstChild.selected).toBe(false); expect(e.lastChild.selected).toBe(true); @@ -268,7 +268,7 @@ describe('ReactDOMServerIntegrationSelect', () => { , - 1, + 2, ); expect(e.firstChild.selected).toBe(true); expect(e.lastChild.selected).toBe(false); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js index f3a8b869ad818..8ea1c9d53baee 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationSpecialTypes-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; let forwardRef; @@ -26,7 +26,7 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); forwardRef = React.forwardRef; @@ -44,7 +44,7 @@ function initModules() { // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js index 697ec7f340d88..dd19385e62c56 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationTextarea-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js index 121ffe93ad25a..55336a2cb5636 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js @@ -15,7 +15,6 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -35,7 +34,6 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => { function initModules() { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); @@ -43,7 +41,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => { // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; @@ -204,7 +202,6 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', ( ReactFeatureFlags.disableJavaScriptURLs = true; React = require('react'); - ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); @@ -212,7 +209,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', ( // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUserInteraction-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUserInteraction-test.js index b335b03b01d38..bc5980f23dda2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUserInteraction-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUserInteraction-test.js @@ -12,7 +12,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -20,13 +20,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index f373213b099bf..8d0023b6f2078 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -776,14 +776,7 @@ describe('ReactDOMServerPartialHydration', () => { const span2 = container.getElementsByTagName('span')[0]; // This is a new node. expect(span).not.toBe(span2); - - if (gate(flags => flags.dfsEffectsRefactor)) { - // The effects list refactor causes this to be null because the Suspense Activity's child - // is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok - expect(ref.current).toBe(null); - } else { - expect(ref.current).toBe(span2); - } + expect(ref.current).toBe(null); // Resolving the promise should render the final content. suspend = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js index 0a9e78fd8c225..537e448f86dd8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.js @@ -11,6 +11,7 @@ let React; let ReactDOM; +let ReactDOMClient; let Suspense; let Scheduler; let act; @@ -23,6 +24,7 @@ describe('ReactDOMSuspensePlaceholder', () => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; Suspense = React.Suspense; @@ -98,7 +100,7 @@ describe('ReactDOMSuspensePlaceholder', () => { return text; } - it('hides and unhides timed out DOM elements', async () => { + it('hides and unhides timed out DOM elements in legacy roots', async () => { const divs = [ React.createRef(null), React.createRef(null), @@ -144,18 +146,22 @@ describe('ReactDOMSuspensePlaceholder', () => { ); } - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.textContent).toEqual('Loading...'); - await act(async () => { - await resolveText('B'); + await act(() => { + resolveText('B'); }); expect(container.textContent).toEqual('ABC'); }); it( - 'outside concurrent mode, re-hides children if their display is updated ' + + 'in legacy roots, re-hides children if their display is updated ' + 'but the boundary is still showing the fallback', async () => { const {useState} = React; @@ -207,7 +213,7 @@ describe('ReactDOMSuspensePlaceholder', () => { ); // Regression test for https://github.com/facebook/react/issues/14188 - it('can call findDOMNode() in a suspended component commit phase', async () => { + it('can call findDOMNode() in a suspended component commit phase in legacy roots', async () => { const log = []; const Lazy = React.lazy( () => @@ -267,7 +273,7 @@ describe('ReactDOMSuspensePlaceholder', () => { }); // Regression test for https://github.com/facebook/react/issues/14188 - it('can call findDOMNode() in a suspended component commit phase (#2)', () => { + it('can call legacy findDOMNode() in a suspended component commit phase (#2)', async () => { let suspendOnce = Promise.resolve(); function Suspend() { if (suspendOnce) { @@ -304,9 +310,16 @@ describe('ReactDOMSuspensePlaceholder', () => { ); } - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(log).toEqual(['cDM']); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); + expect(log).toEqual(['cDM', 'cDU']); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMserverIntegrationProgress-test.js b/packages/react-dom/src/__tests__/ReactDOMserverIntegrationProgress-test.js index b949e0ab522f9..cf51eff4aced3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMserverIntegrationProgress-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMserverIntegrationProgress-test.js @@ -13,7 +13,7 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; -let ReactDOM; +let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; @@ -21,13 +21,13 @@ function initModules() { // Reset warning cache. jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); // Make them available to the helpers. return { - ReactDOM, + ReactDOMClient, ReactDOMServer, ReactTestUtils, }; diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js index 040234f5c8059..403a3de7b6b17 100644 --- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js @@ -12,6 +12,7 @@ let PropTypes; let React; let ReactDOM; +let ReactDOMClient; let act; let ReactFeatureFlags; let Scheduler; @@ -44,6 +45,7 @@ describe('ReactErrorBoundaries', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); React = require('react'); act = require('internal-test-utils').act; Scheduler = require('scheduler'); @@ -576,103 +578,139 @@ describe('ReactErrorBoundaries', () => { }; }); - it('does not swallow exceptions on mounting without boundaries', () => { + it('does not swallow exceptions on mounting without boundaries', async () => { let container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + let root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); }); - it('does not swallow exceptions on updating without boundaries', () => { + it('does not swallow exceptions on updating without boundaries', async () => { let container = document.createElement('div'); - ReactDOM.render(, container); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + let root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); container = document.createElement('div'); - ReactDOM.render(, container); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); container = document.createElement('div'); - ReactDOM.render(, container); - expect(() => { - ReactDOM.render(, container); - }).toThrow('Hello'); + root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + await expect(async () => { + await act(async () => { + root.render(); + }); + }).rejects.toThrow('Hello'); }); - it('does not swallow exceptions on unmounting without boundaries', () => { + it('does not swallow exceptions on unmounting without boundaries', async () => { const container = document.createElement('div'); - ReactDOM.render(, container); - expect(() => { - ReactDOM.unmountComponentAtNode(container); - }).toThrow('Hello'); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + await expect(async () => { + root.unmount(); + }).rejects.toThrow('Hello'); }); - it('prevents errors from leaking into other roots', () => { + it('prevents errors from leaking into other roots', async () => { 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); - ReactDOM.render(Before 1, container1); - expect(() => { - ReactDOM.render(, container2); - }).toThrow('Hello'); - ReactDOM.render( - - - , - container3, - ); + await act(async () => { + root1.render(Before 1); + }); + await expect(async () => { + await act(async () => { + root2.render(); + }); + }).rejects.toThrow('Hello'); + await act(async () => { + root3.render( + + + , + ); + }); expect(container1.firstChild.textContent).toBe('Before 1'); expect(container2.firstChild).toBe(null); expect(container3.firstChild.textContent).toBe('Caught an error: Hello.'); - ReactDOM.render(After 1, container1); - ReactDOM.render(After 2, container2); - ReactDOM.render( - After 3, - container3, - ); + await act(async () => { + root1.render(After 1); + }); + await act(async () => { + root2.render(After 2); + }); + await act(async () => { + root3.render(After 3); + }); expect(container1.firstChild.textContent).toBe('After 1'); expect(container2.firstChild.textContent).toBe('After 2'); expect(container3.firstChild.textContent).toBe('After 3'); - - ReactDOM.unmountComponentAtNode(container1); - ReactDOM.unmountComponentAtNode(container2); - ReactDOM.unmountComponentAtNode(container3); + root1.unmount(); + root2.unmount(); + root3.unmount(); expect(container1.firstChild).toBe(null); expect(container2.firstChild).toBe(null); expect(container3.firstChild).toBe(null); }); - it('logs a single error when using error boundary', () => { + it('logs a single error when using error boundary', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); spyOnDev(console, 'error'); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(1); expect(console.error.mock.calls[0][0]).toContain( - 'ReactDOM.render is no longer supported', - ); - expect(console.error.mock.calls[1][0]).toContain( 'The above error occurred in the component:', ); } @@ -689,21 +727,33 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('renders an error state if child throws in render', () => { + it('renders an error state if child throws in render', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -716,21 +766,33 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('renders an error state if child throws in constructor', () => { + it('renders an error state if child throws in constructor', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -741,21 +803,31 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenConstructor constructor [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('renders an error state if child throws in componentWillMount', () => { + it('renders an error state if child throws in componentWillMount', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -767,15 +839,24 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentWillMount constructor', + 'BrokenComponentWillMount componentWillMount [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); // @gate !disableLegacyContext || !__DEV__ - it('renders an error state if context provider throws in componentWillMount', () => { + it('renders an error state if context provider throws in componentWillMount', async () => { class BrokenComponentWillMountWithContext extends React.Component { static childContextTypes = {foo: PropTypes.number}; getChildContext() { @@ -790,18 +871,20 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); // @gate !disableModulePatternComponents // @gate !disableLegacyContext - it('renders an error state if module-style context provider throws in componentWillMount', () => { + it('renders an error state if module-style context provider throws in componentWillMount', async () => { function BrokenComponentWillMountWithContext() { return { getChildContext() { @@ -820,13 +903,16 @@ describe('ReactErrorBoundaries', () => { }; const container = document.createElement('div'); - expect(() => - ReactDOM.render( - - - , - container, - ), + const root = ReactDOMClient.createRoot(container); + await expect( + async () => + await act(async () => { + root.render( + + + , + ); + }), ).toErrorDev( 'Warning: The component appears to be a function component that ' + 'returns a class instance. ' + @@ -839,19 +925,34 @@ describe('ReactErrorBoundaries', () => { expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); - it('mounts the error message if mounting fails', () => { + it('mounts the error message if mounting fails', async () => { function renderError(error) { return ; } const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); assertLog([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', + 'ErrorMessage constructor', + 'ErrorMessage componentWillMount', + 'ErrorMessage render', + // logs for error retry 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', 'ErrorBoundary render success', @@ -868,23 +969,25 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'ErrorBoundary componentWillUnmount', 'ErrorMessage componentWillUnmount', ]); }); - it('propagates errors on retry on mounting', () => { + it('propagates errors on retry on mounting', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -907,21 +1010,42 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'RetryErrorBoundary constructor', + 'RetryErrorBoundary componentWillMount', + 'RetryErrorBoundary render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'RetryErrorBoundary static getDerivedStateFromError [!]', + 'RetryErrorBoundary componentWillMount', + 'RetryErrorBoundary render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('propagates errors inside boundary during componentWillMount', () => { + it('propagates errors inside boundary during componentWillMount', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -933,23 +1057,34 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentWillMountErrorBoundary constructor', + 'BrokenComponentWillMountErrorBoundary componentWillMount [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('propagates errors inside boundary while rendering error state', () => { + it('propagates errors inside boundary while rendering error state', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -969,23 +1104,41 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRenderErrorBoundary constructor', + 'BrokenRenderErrorBoundary componentWillMount', + 'BrokenRenderErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'BrokenRenderErrorBoundary static getDerivedStateFromError', + 'BrokenRenderErrorBoundary componentWillMount', + 'BrokenRenderErrorBoundary render error [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('does not call componentWillUnmount when aborting initial mount', () => { + it('does not call componentWillUnmount when aborting initial mount', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -1004,14 +1157,27 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'Normal constructor', + 'Normal componentWillMount', + 'Normal render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('resets callback refs if mounting aborts', () => { + it('resets callback refs if mounting aborts', async () => { function childRef(x) { Scheduler.log('Child ref is set to ' + x); } @@ -1020,13 +1186,15 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render( - -
- - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + +
+ + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -1039,29 +1207,41 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'Error message ref is set to [object HTMLDivElement]', 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'ErrorBoundary componentWillUnmount', 'Error message ref is set to null', ]); }); - it('resets object refs if mounting aborts', () => { + it('resets object refs if mounting aborts', async () => { const childRef = React.createRef(); const errorMessageRef = React.createRef(); const container = document.createElement('div'); - ReactDOM.render( - -
- - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + +
+ + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -1074,25 +1254,37 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillMount', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); expect(errorMessageRef.current.toString()).toEqual( '[object HTMLDivElement]', ); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); expect(errorMessageRef.current).toEqual(null); }); - it('successfully mounts if no error occurs', () => { + it('successfully mounts if no error occurs', async () => { const container = document.createElement('div'); - ReactDOM.render( - -
Mounted successfully.
-
, - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + +
Mounted successfully.
+
, + ); + }); expect(container.firstChild.textContent).toBe('Mounted successfully.'); assertLog([ 'ErrorBoundary constructor', @@ -1101,27 +1293,30 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidMount', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches if child throws in constructor during update', () => { + it('catches if child throws in constructor during update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1141,32 +1336,49 @@ describe('ReactErrorBoundaries', () => { // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'Normal componentWillReceiveProps', + 'Normal componentWillUpdate', + 'Normal render', + 'Normal2 constructor', + 'Normal2 componentWillMount', + 'Normal2 render', + 'BrokenConstructor constructor [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches if child throws in componentWillMount during update', () => { + it('catches if child throws in componentWillMount during update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1187,32 +1399,50 @@ describe('ReactErrorBoundaries', () => { // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'Normal componentWillReceiveProps', + 'Normal componentWillUpdate', + 'Normal render', + 'Normal2 constructor', + 'Normal2 componentWillMount', + 'Normal2 render', + 'BrokenComponentWillMount constructor', + 'BrokenComponentWillMount componentWillMount [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches if child throws in componentWillReceiveProps during update', () => { + it('catches if child throws in componentWillReceiveProps during update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - , - container, - ); + await act(async () => { + root.render( + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1228,33 +1458,47 @@ describe('ReactErrorBoundaries', () => { // Render the error message 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'Normal componentWillReceiveProps', + 'Normal componentWillUpdate', + 'Normal render', + 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', 'Normal componentWillUnmount', 'BrokenComponentWillReceiveProps componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches if child throws in componentWillUpdate during update', () => { + it('catches if child throws in componentWillUpdate during update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - , - container, - ); + await act(async () => { + root.render( + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1263,10 +1507,22 @@ describe('ReactErrorBoundaries', () => { 'Normal componentWillReceiveProps', 'Normal componentWillUpdate', 'Normal render', - // BrokenComponentWillUpdate will abort rendering: + // BrokenComponentWillUpdate will abort rendering: + 'BrokenComponentWillUpdate componentWillReceiveProps', + 'BrokenComponentWillUpdate componentWillUpdate [!]', + // Handle the error + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'Normal componentWillReceiveProps', + 'Normal componentWillUpdate', + 'Normal render', 'BrokenComponentWillUpdate componentWillReceiveProps', 'BrokenComponentWillUpdate componentWillUpdate [!]', - // Handle the error 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', @@ -1275,28 +1531,31 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches if child throws in render during update', () => { + it('catches if child throws in render during update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1317,15 +1576,31 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'Normal componentWillReceiveProps', + 'Normal componentWillUpdate', + 'Normal render', + 'Normal2 constructor', + 'Normal2 componentWillMount', + 'Normal2 render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', 'Normal componentWillUnmount', 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('keeps refs up-to-date during updates', () => { + it('keeps refs up-to-date during updates', async () => { function child1Ref(x) { Scheduler.log('Child1 ref is set to ' + x); } @@ -1337,12 +1612,14 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render( - -
- , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + +
+ , + ); + }); assertLog([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', @@ -1351,14 +1628,15 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidMount', ]); - ReactDOM.render( - -
-
- - , - container, - ); + await act(async () => { + root.render( + +
+
+ + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1372,6 +1650,16 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary static getDerivedStateFromError', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render error', + // logs for error retry + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'ErrorBoundary static getDerivedStateFromError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', // Update Child1 ref since Child1 has been unmounted // Child2 ref is never set because its mounting aborted 'Child1 ref is set to null', @@ -1379,31 +1667,34 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'ErrorBoundary componentWillUnmount', 'Error message ref is set to null', ]); }); - it('recovers from componentWillUnmount errors on update', () => { + it('recovers from componentWillUnmount errors on update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1437,31 +1728,34 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('recovers from nested componentWillUnmount errors on update', () => { + it('recovers from nested componentWillUnmount errors on update', async () => { const container = document.createElement('div'); - ReactDOM.render( - - + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + - - - , - container, - ); + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1496,11 +1790,11 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('picks the right boundary when handling unmounting errors', () => { + it('picks the right boundary when handling unmounting errors', async () => { function renderInnerError(error) { return
Caught an inner error: {error.message}.
; } @@ -1509,31 +1803,34 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render( - + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( - - - , - container, - ); + logName="OuterErrorBoundary" + renderError={renderOuterError}> + + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - + await act(async () => { + root.render( - , - container, - ); + logName="OuterErrorBoundary" + renderError={renderOuterError}> + + , + ); + }); expect(container.textContent).toBe('Caught an inner error: Hello.'); assertLog([ // Update outer boundary @@ -1559,39 +1856,43 @@ describe('ReactErrorBoundaries', () => { 'InnerErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'OuterErrorBoundary componentWillUnmount', 'InnerErrorBoundary componentWillUnmount', ]); }); - it('can recover from error state', () => { + it('can recover from error state', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); // Error boundary doesn't retry by itself: expect(container.textContent).toBe('Caught an error: Hello.'); // Force the success path: Scheduler.unstable_clearLog(); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); expect(container.textContent).not.toContain('Caught an error'); assertLog([ 'ErrorBoundary componentWillReceiveProps', @@ -1606,75 +1907,88 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'ErrorBoundary componentWillUnmount', 'Normal componentWillUnmount', ]); }); - it('can update multiple times in error state', () => { + it('can update multiple times in error state', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); - ReactDOM.render(
Other screen
, container); + await act(async () => { + root.render(
Other screen
); + }); expect(container.textContent).toBe('Other screen'); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); }); - it("doesn't get into inconsistent state during removals", () => { + it("doesn't get into inconsistent state during removals", async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + , + ); + }); - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.textContent).toBe('Caught an error: Hello.'); Scheduler.unstable_clearLog(); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it("doesn't get into inconsistent state during additions", () => { + it("doesn't get into inconsistent state during additions", async () => { const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render( - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); Scheduler.unstable_clearLog(); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it("doesn't get into inconsistent state during reorders", () => { + it("doesn't get into inconsistent state during reorders", async () => { function getAMixOfNormalAndBrokenRenderElements() { const elements = []; for (let i = 0; i < 100; i++) { @@ -1704,25 +2018,32 @@ describe('ReactErrorBoundaries', () => { let fail = false; const container = document.createElement('div'); - ReactDOM.render( - {getAMixOfNormalAndBrokenRenderElements()}, - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + {getAMixOfNormalAndBrokenRenderElements()} + , + ); + }); expect(container.textContent).not.toContain('Caught an error'); fail = true; - ReactDOM.render( - {getAMixOfNormalAndBrokenRenderElements()}, - container, - ); + await act(async () => { + root.render( + + {getAMixOfNormalAndBrokenRenderElements()} + , + ); + }); expect(container.textContent).toBe('Caught an error: Hello.'); Scheduler.unstable_clearLog(); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches errors originating downstream', () => { + it('catches errors originating downstream', async () => { let fail = false; class Stateful extends React.Component { state = {shouldThrow: false}; @@ -1738,43 +2059,38 @@ describe('ReactErrorBoundaries', () => { let statefulInst; const container = document.createElement('div'); - ReactDOM.render( - - (statefulInst = inst)} /> - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + (statefulInst = inst)} /> + , + ); + }); Scheduler.unstable_clearLog(); expect(() => { fail = true; statefulInst.forceUpdate(); }).not.toThrow(); - - assertLog([ - 'Stateful render [!]', - 'ErrorBoundary static getDerivedStateFromError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - 'ErrorBoundary componentDidUpdate', - ]); - - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches errors in componentDidMount', () => { + it('catches errors in componentDidMount', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + + + , + ); + }); assertLog([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', @@ -1817,26 +2133,29 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('catches errors in componentDidUpdate', () => { + it('catches errors in componentDidUpdate', async () => { const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); assertLog([ 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', @@ -1855,33 +2174,29 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); it('catches errors in useEffect', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); await act(() => { - ReactDOM.render( + root.render( Initial value , - container, ); - assertLog([ - 'ErrorBoundary constructor', - 'ErrorBoundary componentWillMount', - 'ErrorBoundary render success', - 'BrokenUseEffect render', - 'ErrorBoundary componentDidMount', - ]); - - expect(container.firstChild.textContent).toBe('Initial value'); - Scheduler.unstable_clearLog(); }); // verify flushed passive effects and handle the error assertLog([ + // logs for error retry + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenUseEffect render', + 'ErrorBoundary componentDidMount', 'BrokenUseEffect useEffect [!]', // Handle the error 'ErrorBoundary static getDerivedStateFromError', @@ -1889,18 +2204,19 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary render error', 'ErrorBoundary componentDidUpdate', ]); - expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); - it('catches errors in useLayoutEffect', () => { + it('catches errors in useLayoutEffect', async () => { const container = document.createElement('div'); - ReactDOM.render( - - Initial value - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + Initial value + , + ); + }); assertLog([ 'ErrorBoundary constructor', 'ErrorBoundary componentWillMount', @@ -1920,18 +2236,20 @@ describe('ReactErrorBoundaries', () => { expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); - it('propagates errors inside boundary during componentDidMount', () => { + it('propagates errors inside boundary during componentDidMount', async () => { const container = document.createElement('div'); - ReactDOM.render( - - ( -
We should never catch our own error: {error.message}.
- )} - /> -
, - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + ( +
We should never catch our own error: {error.message}.
+ )} + /> +
, + ); + }); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); assertLog([ 'ErrorBoundary constructor', @@ -1952,11 +2270,11 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog(['ErrorBoundary componentWillUnmount']); }); - it('calls static getDerivedStateFromError for each error that is captured', () => { + it('calls static getDerivedStateFromError for each error that is captured', async () => { function renderUnmountError(error) { return
Caught an unmounting error: {error.message}.
; } @@ -1965,40 +2283,43 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render( - - - - - - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + + + + + + + + , + ); + }); Scheduler.unstable_clearLog(); - ReactDOM.render( - - - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + + + , + ); + }); expect(container.firstChild.textContent).toBe( 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.', @@ -2048,7 +2369,7 @@ describe('ReactErrorBoundaries', () => { 'InnerUpdateBoundary componentDidUpdate', ]); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); assertLog([ 'OuterErrorBoundary componentWillUnmount', 'InnerUnmountBoundary componentWillUnmount', @@ -2056,7 +2377,7 @@ describe('ReactErrorBoundaries', () => { ]); }); - it('discards a bad root if the root component fails', () => { + it('discards a bad root if the root component fails', async () => { const X = null; const Y = undefined; let err1; @@ -2064,7 +2385,13 @@ describe('ReactErrorBoundaries', () => { try { const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect( + async () => + await act(async () => { + root.render(, container); + }), + ).toErrorDev( 'React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function ' + '(for composite components) but got: null.', @@ -2074,7 +2401,13 @@ describe('ReactErrorBoundaries', () => { } try { const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await expect( + async () => + await act(async () => { + root.render(, container); + }), + ).toErrorDev( 'React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function ' + '(for composite components) but got: undefined.', @@ -2087,19 +2420,23 @@ describe('ReactErrorBoundaries', () => { expect(err2.message).toMatch(/got: undefined/); }); - it('renders empty output if error boundary does not handle the error', () => { + it('renders empty output if error boundary does not handle the error', async () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render( -
- Sibling - - - -
, - container, - ), - ).toThrow('Hello'); + const root = ReactDOMClient.createRoot(container); + + await expect(async () => { + await act(async () => { + root.render( +
+ Sibling + + + +
, + ); + }); + }).rejects.toThrow('Hello'); + expect(container.innerHTML).toBe(''); assertLog([ 'NoopErrorBoundary constructor', @@ -2114,10 +2451,22 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', + // logs for error retry + 'NoopErrorBoundary constructor', + 'NoopErrorBoundary componentWillMount', + 'NoopErrorBoundary render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', + 'NoopErrorBoundary static getDerivedStateFromError', + 'NoopErrorBoundary render', + 'BrokenRender constructor', + 'BrokenRender componentWillMount', + 'BrokenRender render [!]', ]); }); - it('passes first error when two errors happen in commit', () => { + it('passes first error when two errors happen in commit', async () => { const errors = []; let caughtError; class Parent extends React.Component { @@ -2140,10 +2489,13 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); try { // Here, we test the behavior where there is no error boundary and we // delegate to the host root. - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); } catch (e) { if (e.message !== 'parent sad' && e.message !== 'child sad') { throw e; @@ -2156,19 +2508,22 @@ describe('ReactErrorBoundaries', () => { expect(caughtError.message).toBe('child sad'); }); - it('propagates uncaught error inside unbatched initial mount', () => { + it('propagates uncaught error inside unbatched initial mount', async () => { function Foo() { throw new Error('foo error'); } const container = document.createElement('div'); - expect(() => { - ReactDOM.unstable_batchedUpdates(() => { - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await ReactDOM.unstable_batchedUpdates(async () => { + await act(async () => { + root.render(); + }); }); - }).toThrow('foo error'); + }).rejects.toThrow('foo error'); }); - it('handles errors that occur in before-mutation commit hook', () => { + it('handles errors that occur in before-mutation commit hook', async () => { const errors = []; let caughtError; class Parent extends React.Component { @@ -2193,9 +2548,14 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render(); + }); try { - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); } catch (e) { if (e.message !== 'parent sad' && e.message !== 'child sad') { throw e; @@ -2208,7 +2568,7 @@ describe('ReactErrorBoundaries', () => { expect(caughtError.message).toBe('child sad'); }); - it('should warn if an error boundary with only componentDidCatch does not update state', () => { + it('should warn if an error boundary with only componentDidCatch does not update state', async () => { class InvalidErrorBoundary extends React.Component { componentDidCatch(error, info) { // This component does not define getDerivedStateFromError(). @@ -2225,13 +2585,15 @@ describe('ReactErrorBoundaries', () => { }; const container = document.createElement('div'); - expect(() => { - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(async () => { + root.render( + + + , + ); + }); }).toErrorDev( 'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' + 'In that method, return a state update to display an error message or fallback UI.', @@ -2239,7 +2601,7 @@ describe('ReactErrorBoundaries', () => { expect(container.textContent).toBe(''); }); - it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', () => { + it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', async () => { let componentDidCatchError, getDerivedStateFromErrorError; class ErrorBoundaryWithBothMethods extends React.Component { state = {error: null}; @@ -2261,34 +2623,39 @@ describe('ReactErrorBoundaries', () => { }; const container = document.createElement('div'); - ReactDOM.render( - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + + , + ); + }); expect(container.textContent).toBe('ErrorBoundary'); expect(componentDidCatchError).toBe(thrownError); expect(getDerivedStateFromErrorError).toBe(thrownError); }); - it('should catch errors from invariants in completion phase', () => { + it('should catch errors from invariants in completion phase', async () => { const container = document.createElement('div'); - ReactDOM.render( - - -
- - , - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(async () => { + root.render( + + +
+ + , + ); + }); expect(container.textContent).toContain( 'Caught an error: input is a void element tag', ); }); - it('should catch errors from errors in the throw phase from boundaries', () => { + it('should catch errors from errors in the throw phase from boundaries', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); const thrownError = new Error('original error'); const Throws = () => { @@ -2304,22 +2671,24 @@ describe('ReactErrorBoundaries', () => { } } - ReactDOM.render( - - - - - , - container, - ); + await act(async () => { + root.render( + + + + + , + ); + }); expect(container.textContent).toContain( 'Caught an error: gotta catch em all', ); }); - it('should protect errors from errors in the stack generation', () => { + it('should protect errors from errors in the stack generation', async () => { const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); const evilError = { message: 'gotta catch em all', @@ -2340,19 +2709,22 @@ describe('ReactErrorBoundaries', () => { return ; } - ReactDOM.render( - - - , - container, - ); + await expect(async () => { + await act(async () => { + root.render( + + + , + ); + }); + }).rejects.toThrow('gotta catch em all'); expect(container.textContent).toContain( 'Caught an error: gotta catch em all.', ); }); - it('catches errors thrown in componentWillUnmount', () => { + it('catches errors thrown in componentWillUnmount', async () => { class LocalErrorBoundary extends React.Component { state = {error: null}; static getDerivedStateFromError(error) { @@ -2392,16 +2764,18 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - - ReactDOM.render( - - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + + await act(async () => { + root.render( + + + + + + , + ); + }); expect(container.firstChild.textContent).toBe('sibling'); expect(container.lastChild.textContent).toBe('broken'); @@ -2412,12 +2786,13 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillUnmount render', ]); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); // React should skip over the unmounting boundary and find the nearest still-mounted boundary. expect(container.firstChild.textContent).toBe('OuterFallback'); @@ -2432,7 +2807,7 @@ describe('ReactErrorBoundaries', () => { ]); }); - it('catches errors thrown while detaching refs', () => { + it('catches errors thrown while detaching refs', async () => { class LocalErrorBoundary extends React.Component { state = {error: null}; static getDerivedStateFromError(error) { @@ -2474,16 +2849,18 @@ describe('ReactErrorBoundaries', () => { } const container = document.createElement('div'); - - ReactDOM.render( - - - - - - , - container, - ); + const root = ReactDOMClient.createRoot(container); + + await act(async () => { + root.render( + + + + + + , + ); + }); expect(container.firstChild.textContent).toBe('sibling'); expect(container.lastChild.textContent).toBe('ref'); @@ -2495,12 +2872,13 @@ describe('ReactErrorBoundaries', () => { 'LocalBrokenCallbackRef ref true', ]); - ReactDOM.render( - - - , - container, - ); + await act(async () => { + root.render( + + + , + ); + }); // React should skip over the unmounting boundary and find the nearest still-mounted boundary. expect(container.firstChild.textContent).toBe('OuterFallback'); diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundariesHooks-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundariesHooks-test.internal.js index 63558ea98acd2..4a5679e5fcff2 100644 --- a/packages/react-dom/src/__tests__/ReactErrorBoundariesHooks-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactErrorBoundariesHooks-test.internal.js @@ -10,16 +10,18 @@ 'use strict'; let React; -let ReactDOM; +let ReactDOMClient; +let act; describe('ReactErrorBoundariesHooks', () => { beforeEach(() => { jest.resetModules(); - ReactDOM = require('react-dom'); React = require('react'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; }); - it('should preserve hook order if errors are caught', () => { + it('should preserve hook order if errors are caught', async () => { function ErrorThrower() { React.useMemo(() => undefined, []); throw new Error('expected'); @@ -57,10 +59,15 @@ describe('ReactErrorBoundariesHooks', () => { } const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); - expect(() => { - ReactDOM.render(, container); - }).not.toThrow(); + await expect( + act(() => { + root.render(); + }), + ).resolves.not.toThrow(); }); }); diff --git a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js index d1f5493e95f44..42908a693cce3 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js @@ -11,16 +11,18 @@ let React; let ReactDOM; -let ReactTestUtils; +let ReactDOMClient; let PropTypes; +let act; describe('ReactLegacyCompositeComponent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); - ReactTestUtils = require('react-dom/test-utils'); + ReactDOMClient = require('react-dom/client'); PropTypes = require('prop-types'); + act = require('internal-test-utils').act; }); it('should warn about `setState` in render in legacy mode', () => { @@ -70,7 +72,7 @@ describe('ReactLegacyCompositeComponent', () => { }); // @gate !disableLegacyContext - it('should pass context to children when not owner', () => { + it('should pass context to children when not owner', async () => { class Parent extends React.Component { render() { return ( @@ -106,13 +108,17 @@ describe('ReactLegacyCompositeComponent', () => { return
{this.context.foo}
; } } - - const component = ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + let component; + await act(() => { + root.render( (component = current)} />); + }); expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar'); }); // @gate !disableLegacyContext - it('should pass context when re-rendered for static child', () => { + it('should pass context when re-rendered for static child', async () => { let parentInstance = null; let childInstance = null; @@ -156,24 +162,31 @@ describe('ReactLegacyCompositeComponent', () => { } } - parentInstance = ReactTestUtils.renderIntoDocument( - - - - - , - ); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( + (parentInstance = current)}> + + + + , + ); + }); expect(parentInstance.state.flag).toBe(false); expect(childInstance.context).toEqual({foo: 'bar', flag: false}); - parentInstance.setState({flag: true}); + await act(() => { + parentInstance.setState({flag: true}); + }); expect(parentInstance.state.flag).toBe(true); expect(childInstance.context).toEqual({foo: 'bar', flag: true}); }); // @gate !disableLegacyContext - it('should pass context when re-rendered for static child within a composite component', () => { + it('should pass context when re-rendered for static child within a composite component', async () => { class Parent extends React.Component { static childContextTypes = { flag: PropTypes.bool, @@ -217,20 +230,27 @@ describe('ReactLegacyCompositeComponent', () => { } } - const wrapper = ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + let wrapper; + await act(() => { + root.render( (wrapper = current)} />); + }); expect(wrapper.parentRef.current.state.flag).toEqual(true); expect(wrapper.childRef.current.context).toEqual({flag: true}); // We update while is still a static prop relative to this update - wrapper.parentRef.current.setState({flag: false}); + await act(() => { + wrapper.parentRef.current.setState({flag: false}); + }); expect(wrapper.parentRef.current.state.flag).toEqual(false); expect(wrapper.childRef.current.context).toEqual({flag: false}); }); // @gate !disableLegacyContext - it('should pass context transitively', () => { + it('should pass context transitively', async () => { let childInstance = null; let grandchildInstance = null; @@ -286,13 +306,18 @@ describe('ReactLegacyCompositeComponent', () => { } } - ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1}); }); // @gate !disableLegacyContext - it('should pass context when re-rendered', () => { + it('should pass context when re-rendered', async () => { let parentInstance = null; let childInstance = null; @@ -334,11 +359,16 @@ describe('ReactLegacyCompositeComponent', () => { } } - parentInstance = ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( (parentInstance = current)} />); + }); + expect(childInstance).toBeNull(); expect(parentInstance.state.flag).toBe(false); - ReactDOM.unstable_batchedUpdates(function () { + await act(() => { parentInstance.setState({flag: true}); }); expect(parentInstance.state.flag).toBe(true); @@ -699,7 +729,7 @@ describe('ReactLegacyCompositeComponent', () => { ); }); - it('should replace state in legacy mode', () => { + it('should replace state in legacy mode', async () => { class Moo extends React.Component { state = {x: 1}; render() { @@ -707,15 +737,23 @@ describe('ReactLegacyCompositeComponent', () => { } } - const moo = ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + let moo; + await act(() => { + root.render( (moo = current)} />); + }); + // No longer a public API, but we can test that it works internally by // reaching into the updater. - moo.updater.enqueueReplaceState(moo, {y: 2}); + await act(() => { + moo.updater.enqueueReplaceState(moo, {y: 2}); + }); expect('x' in moo.state).toBe(false); expect(moo.state.y).toBe(2); }); - it('should support objects with prototypes as state in legacy mode', () => { + it('should support objects with prototypes as state in legacy mode', async () => { const NotActuallyImmutable = function (str) { this.str = str; }; @@ -732,24 +770,34 @@ describe('ReactLegacyCompositeComponent', () => { } } - const moo = ReactTestUtils.renderIntoDocument(); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + let moo; + await act(() => { + root.render( (moo = current)} />); + }); + expect(moo.state.str).toBe('first'); expect(moo.state.amIImmutable()).toBe(true); const secondState = new NotActuallyImmutable('second'); - moo._replaceState(secondState); + await act(() => { + moo._replaceState(secondState); + }); expect(moo.state.str).toBe('second'); expect(moo.state.amIImmutable()).toBe(true); expect(moo.state).toBe(secondState); - moo.setState({str: 'third'}); + await act(() => { + moo.setState({str: 'third'}); + }); expect(moo.state.str).toBe('third'); // Here we lose the prototype. expect(moo.state.amIImmutable).toBe(undefined); // When more than one state update is enqueued, we have the same behavior const fifthState = new NotActuallyImmutable('fifth'); - ReactDOM.unstable_batchedUpdates(function () { + await act(() => { moo.setState({str: 'fourth'}); moo._replaceState(fifthState); }); @@ -757,7 +805,7 @@ describe('ReactLegacyCompositeComponent', () => { // When more than one state update is enqueued, we have the same behavior const sixthState = new NotActuallyImmutable('sixth'); - ReactDOM.unstable_batchedUpdates(function () { + await act(() => { moo._replaceState(sixthState); moo.setState({str: 'seventh'}); }); diff --git a/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js b/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js index 0bfb27ade197b..3a2a5126882ce 100644 --- a/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js +++ b/packages/react-dom/src/__tests__/ReactMultiChildReconcile-test.js @@ -10,7 +10,8 @@ 'use strict'; const React = require('react'); -const ReactDOM = require('react-dom'); +const ReactDOMClient = require('react-dom/client'); +const act = require('internal-test-utils').act; const stripEmptyValues = function (obj) { const ret = {}; @@ -221,24 +222,40 @@ function verifyDomOrderingAccurate(outerContainer, statusDisplays) { expect(orderedDomKeys).toEqual(orderedLogicalKeys); } -function testPropsSequenceWithPreparedChildren(sequence, prepareChildren) { +async function testPropsSequenceWithPreparedChildren( + sequence, + prepareChildren, +) { const container = document.createElement('div'); - const parentInstance = ReactDOM.render( - , - container, - ); + const root = ReactDOMClient.createRoot(container); + let parentInstance; + await act(() => { + root.render( + { + if (parentInstance === undefined) { + parentInstance = current; + } + }} + />, + ); + }); let statusDisplays = parentInstance.getStatusDisplays(); let lastInternalStates = getInternalStateByUserName(statusDisplays); verifyStatuses(statusDisplays, sequence[0]); for (let i = 1; i < sequence.length; i++) { - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); verifyStatuses(statusDisplays, sequence[i]); verifyStatesPreserved(lastInternalStates, statusDisplays); @@ -274,13 +291,13 @@ function prepareChildrenModernIterable(childrenArray) { }; } -function testPropsSequence(sequence) { - testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray); - testPropsSequenceWithPreparedChildren( +async function testPropsSequence(sequence) { + await testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray); + await testPropsSequenceWithPreparedChildren( sequence, prepareChildrenLegacyIterable, ); - testPropsSequenceWithPreparedChildren( + await testPropsSequenceWithPreparedChildren( sequence, prepareChildrenModernIterable, ); @@ -291,7 +308,7 @@ describe('ReactMultiChildReconcile', () => { jest.resetModules(); }); - it('should reset internal state if removed then readded in an array', () => { + it('should reset internal state if removed then readded in an array', async () => { // Test basics. const props = { usernameToStatus: { @@ -300,32 +317,44 @@ describe('ReactMultiChildReconcile', () => { }; const container = document.createElement('div'); - const parentInstance = ReactDOM.render( - , - container, - ); + const root = ReactDOMClient.createRoot(container); + let parentInstance; + await act(() => { + root.render( + { + if (parentInstance === undefined) { + parentInstance = current; + } + }} + />, + ); + }); let statusDisplays = parentInstance.getStatusDisplays(); const startingInternalState = statusDisplays.jcw.getInternalState(); // Now remove the child. - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeFalsy(); // Now reset the props that cause there to be a child - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeTruthy(); expect(statusDisplays.jcw.getInternalState()).not.toBe( @@ -333,7 +362,7 @@ describe('ReactMultiChildReconcile', () => { ); }); - it('should reset internal state if removed then readded in a legacy iterable', () => { + it('should reset internal state if removed then readded in a legacy iterable', async () => { // Test basics. const props = { usernameToStatus: { @@ -342,32 +371,47 @@ describe('ReactMultiChildReconcile', () => { }; const container = document.createElement('div'); - const parentInstance = ReactDOM.render( - , - container, - ); + const root = ReactDOMClient.createRoot(container); + let parentInstance; + await act(() => { + root.render( + { + if (parentInstance === undefined) { + parentInstance = current; + } + }} + />, + ); + }); + let statusDisplays = parentInstance.getStatusDisplays(); const startingInternalState = statusDisplays.jcw.getInternalState(); // Now remove the child. - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeFalsy(); // Now reset the props that cause there to be a child - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeTruthy(); expect(statusDisplays.jcw.getInternalState()).not.toBe( @@ -375,7 +419,7 @@ describe('ReactMultiChildReconcile', () => { ); }); - it('should reset internal state if removed then readded in a modern iterable', () => { + it('should reset internal state if removed then readded in a modern iterable', async () => { // Test basics. const props = { usernameToStatus: { @@ -384,32 +428,47 @@ describe('ReactMultiChildReconcile', () => { }; const container = document.createElement('div'); - const parentInstance = ReactDOM.render( - , - container, - ); + const root = ReactDOMClient.createRoot(container); + let parentInstance; + await act(() => { + root.render( + { + if (parentInstance === undefined) { + parentInstance = current; + } + }} + />, + ); + }); + let statusDisplays = parentInstance.getStatusDisplays(); const startingInternalState = statusDisplays.jcw.getInternalState(); // Now remove the child. - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeFalsy(); // Now reset the props that cause there to be a child - ReactDOM.render( - , - container, - ); + await act(() => { + root.render( + , + ); + }); + statusDisplays = parentInstance.getStatusDisplays(); expect(statusDisplays.jcw).toBeTruthy(); expect(statusDisplays.jcw.getInternalState()).not.toBe( @@ -417,7 +476,7 @@ describe('ReactMultiChildReconcile', () => { ); }); - it('should create unique identity', () => { + it('should create unique identity', async () => { // Test basics. const usernameToStatus = { jcw: 'jcwStatus', @@ -425,10 +484,10 @@ describe('ReactMultiChildReconcile', () => { bob: 'bobStatus', }; - testPropsSequence([{usernameToStatus: usernameToStatus}]); + await testPropsSequence([{usernameToStatus: usernameToStatus}]); }); - it('should preserve order if children order has not changed', () => { + it('should preserve order if children order has not changed', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -443,10 +502,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should transition from zero to one children correctly', () => { + it('should transition from zero to one children correctly', async () => { const PROPS_SEQUENCE = [ {usernameToStatus: {}}, { @@ -455,10 +514,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should transition from one to zero children correctly', () => { + it('should transition from one to zero children correctly', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -467,11 +526,11 @@ describe('ReactMultiChildReconcile', () => { }, {usernameToStatus: {}}, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should transition from one child to null children', () => { - testPropsSequence([ + it('should transition from one child to null children', async () => { + await testPropsSequence([ { usernameToStatus: { first: 'firstStatus', @@ -481,8 +540,8 @@ describe('ReactMultiChildReconcile', () => { ]); }); - it('should transition from null children to one child', () => { - testPropsSequence([ + it('should transition from null children to one child', async () => { + await testPropsSequence([ {}, { usernameToStatus: { @@ -492,8 +551,8 @@ describe('ReactMultiChildReconcile', () => { ]); }); - it('should transition from zero children to null children', () => { - testPropsSequence([ + it('should transition from zero children to null children', async () => { + await testPropsSequence([ { usernameToStatus: {}, }, @@ -501,8 +560,8 @@ describe('ReactMultiChildReconcile', () => { ]); }); - it('should transition from null children to zero children', () => { - testPropsSequence([ + it('should transition from null children to zero children', async () => { + await testPropsSequence([ {}, { usernameToStatus: {}, @@ -514,7 +573,7 @@ describe('ReactMultiChildReconcile', () => { * `FriendsStatusDisplay` renders nulls as empty children (it's a convention * of `FriendsStatusDisplay`, nothing related to React or these test cases. */ - it('should remove nulled out children at the beginning', () => { + it('should remove nulled out children at the beginning', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -529,10 +588,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should remove nulled out children at the end', () => { + it('should remove nulled out children at the end', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -547,10 +606,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should reverse the order of two children', () => { + it('should reverse the order of two children', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -565,10 +624,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should reverse the order of more than two children', () => { + it('should reverse the order of more than two children', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -585,10 +644,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should cycle order correctly', () => { + it('should cycle order correctly', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -632,10 +691,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should cycle order correctly in the other direction', () => { + it('should cycle order correctly in the other direction', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -679,10 +738,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should remove nulled out children and ignore new null children', () => { + it('should remove nulled out children and ignore new null children', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -698,10 +757,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should remove nulled out children and reorder remaining', () => { + it('should remove nulled out children and reorder remaining', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -719,10 +778,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should append children to the end', () => { + it('should append children to the end', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -738,10 +797,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should append multiple children to the end', () => { + it('should append multiple children to the end', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -758,10 +817,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should prepend children to the beginning', () => { + it('should prepend children to the beginning', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -777,10 +836,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should prepend multiple children to the beginning', () => { + it('should prepend multiple children to the beginning', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -797,10 +856,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should not prepend an empty child to the beginning', () => { + it('should not prepend an empty child to the beginning', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -816,10 +875,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should not append an empty child to the end', () => { + it('should not append an empty child to the end', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -835,10 +894,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should not insert empty children in the middle', () => { + it('should not insert empty children in the middle', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -856,10 +915,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should insert one new child in the middle', () => { + it('should insert one new child in the middle', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -875,10 +934,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should insert multiple new truthy children in the middle', () => { + it('should insert multiple new truthy children in the middle', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -896,10 +955,10 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); - it('should insert non-empty children in middle where nulls were', () => { + it('should insert non-empty children in middle where nulls were', async () => { const PROPS_SEQUENCE = [ { usernameToStatus: { @@ -920,6 +979,6 @@ describe('ReactMultiChildReconcile', () => { }, }, ]; - testPropsSequence(PROPS_SEQUENCE); + await testPropsSequence(PROPS_SEQUENCE); }); }); diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js index d7ed05673c67f..45babfd4032d3 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsActUnmockedScheduler-test.js @@ -32,7 +32,7 @@ beforeEach(() => { yields = []; React = require('react'); ReactDOMClient = require('react-dom/client'); - act = React.unstable_act; + act = React.act; container = document.createElement('div'); document.body.appendChild(container); }); diff --git a/packages/react-dom/src/__tests__/multiple-copies-of-react-test.js b/packages/react-dom/src/__tests__/multiple-copies-of-react-test.js index 1de130b2fed2d..96647eed81cf4 100644 --- a/packages/react-dom/src/__tests__/multiple-copies-of-react-test.js +++ b/packages/react-dom/src/__tests__/multiple-copies-of-react-test.js @@ -10,7 +10,8 @@ 'use strict'; let React = require('react'); -const ReactTestUtils = require('react-dom/test-utils'); +const ReactDOMClient = require('react-dom/client'); +const act = require('internal-test-utils').act; class TextWithStringRef extends React.Component { render() { @@ -21,10 +22,14 @@ class TextWithStringRef extends React.Component { } describe('when different React version is used with string ref', () => { - it('throws the "Refs must have owner" warning', () => { - expect(() => { - ReactTestUtils.renderIntoDocument(); - }).toThrow( + it('throws the "Refs must have owner" warning', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect( + act(() => { + root.render(); + }), + ).rejects.toThrow( 'Element ref was specified as a string (foo) but no owner was set. This could happen for one of' + ' the following reasons:\n' + '1. You may be adding a ref to a function component\n' + diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 8392b57af03cf..e9c4479b028fb 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -14,11 +14,12 @@ const shouldIgnoreConsoleError = require('../../../../../scripts/jest/shouldIgno module.exports = function (initModules) { let ReactDOM; + let ReactDOMClient; let ReactDOMServer; let act; function resetModules() { - ({ReactDOM, ReactDOMServer} = initModules()); + ({ReactDOM, ReactDOMClient, ReactDOMServer} = initModules()); act = require('internal-test-utils').act; } @@ -51,11 +52,24 @@ module.exports = function (initModules) { async function asyncReactDOMRender(reactElement, domElement, forceHydrate) { if (forceHydrate) { await act(() => { - ReactDOM.hydrate(reactElement, domElement); + if (ReactDOMClient) { + ReactDOMClient.hydrateRoot(domElement, reactElement, { + onRecoverableError: () => { + // TODO: assert on recoverable error count. + }, + }); + } else { + ReactDOM.hydrate(reactElement, domElement); + } }); } else { await act(() => { - ReactDOM.render(reactElement, domElement); + if (ReactDOMClient) { + const root = ReactDOMClient.createRoot(domElement); + root.render(reactElement); + } else { + ReactDOM.render(reactElement, domElement); + } }); } } @@ -80,7 +94,11 @@ module.exports = function (initModules) { for (let i = 0; i < console.error.mock.calls.length; i++) { const args = console.error.mock.calls[i]; const [format, ...rest] = args; - if (!shouldIgnoreConsoleError(format, rest)) { + if ( + !shouldIgnoreConsoleError(format, rest, { + TODO_ignoreHydrationErrors: true, + }) + ) { filteredWarnings.push(args); } } diff --git a/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js index 30064ecad99d8..aae6b02239eba 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js @@ -10,14 +10,16 @@ 'use strict'; let React; -let ReactDOM; +let ReactDOMClient; +let act; describe('SyntheticClipboardEvent', () => { let container; beforeEach(() => { React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; // The container has to be attached for events to fire. container = document.createElement('div'); @@ -32,7 +34,7 @@ describe('SyntheticClipboardEvent', () => { describe('ClipboardEvent interface', () => { describe('clipboardData', () => { describe('when event has clipboardData', () => { - it("returns event's clipboardData", () => { + it("returns event's clipboardData", async () => { let expectedCount = 0; // Mock clipboardData since jsdom implementation doesn't have a constructor @@ -47,30 +49,39 @@ describe('SyntheticClipboardEvent', () => { expect(event.clipboardData).toBe(clipboardData); expectedCount++; }; - const div = ReactDOM.render( -
, - container, - ); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
, + ); + }); + + const div = container.firstChild; let event; event = document.createEvent('Event'); event.initEvent('copy', true, true); event.clipboardData = clipboardData; - div.dispatchEvent(event); - + await act(() => { + div.dispatchEvent(event); + }); event = document.createEvent('Event'); event.initEvent('cut', true, true); event.clipboardData = clipboardData; - div.dispatchEvent(event); + await act(() => { + div.dispatchEvent(event); + }); event = document.createEvent('Event'); event.initEvent('paste', true, true); event.clipboardData = clipboardData; - div.dispatchEvent(event); + await act(() => { + div.dispatchEvent(event); + }); expect(expectedCount).toBe(3); }); @@ -79,7 +90,7 @@ describe('SyntheticClipboardEvent', () => { }); describe('EventInterface', () => { - it('is able to `preventDefault` and `stopPropagation`', () => { + it('is able to `preventDefault` and `stopPropagation`', async () => { let expectedCount = 0; const eventHandler = event => { @@ -92,14 +103,19 @@ describe('SyntheticClipboardEvent', () => { expectedCount++; }; - const div = ReactDOM.render( -
, - container, - ); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( +
, + ); + }); + + const div = container.firstChild; let event; event = document.createEvent('Event'); diff --git a/packages/react-dom/src/events/__tests__/SyntheticFocusEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticFocusEvent-test.js index 9e70ca1486439..ef889c4b60d10 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticFocusEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticFocusEvent-test.js @@ -9,13 +9,15 @@ describe('SyntheticFocusEvent', () => { let React; - let ReactDOM; + let ReactDOMClient; + let act; let container; beforeEach(() => { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); + act = require('internal-test-utils').act; container = document.createElement('div'); document.body.appendChild(container); @@ -26,44 +28,54 @@ describe('SyntheticFocusEvent', () => { container = null; }); - test('onFocus events have the focus type', () => { + test('onFocus events have the focus type', async () => { const log = []; - ReactDOM.render( -