diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index a0500f47e9603..c7a191943abdf 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -8,18 +8,23 @@ */ let React; -let TestUtils; +let ReactDOM; let TestRenderer; global.__DEV__ = process.env.NODE_ENV !== 'production'; +jest.mock('react-dom', () => + require.requireActual('react-dom/cjs/react-dom-testing.development.js') +); +// we'll replace the above with react/testing and react-dom/testing right before the next minor + expect.extend(require('../toWarnDev')); describe('unmocked scheduler', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - TestUtils = require('react-dom/test-utils'); + ReactDOM = require('react-dom'); TestRenderer = require('react-test-renderer'); }); @@ -33,7 +38,7 @@ describe('unmocked scheduler', () => { } // in legacy mode, this tests whether an act only flushes its own effects TestRenderer.act(() => { - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.create(); }); expect(log).toEqual([]); @@ -42,7 +47,7 @@ describe('unmocked scheduler', () => { log = []; // for doublechecking, we flip it inside out, and assert on the outermost - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.act(() => { TestRenderer.create(); }); @@ -59,7 +64,7 @@ describe('mocked scheduler', () => { require.requireActual('scheduler/unstable_mock') ); React = require('react'); - TestUtils = require('react-dom/test-utils'); + ReactDOM = require('react-dom'); TestRenderer = require('react-test-renderer'); }); @@ -77,7 +82,7 @@ describe('mocked scheduler', () => { } // with a mocked scheduler, this tests whether it flushes all work only on the outermost act TestRenderer.act(() => { - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.create(); }); expect(log).toEqual([]); @@ -86,7 +91,7 @@ describe('mocked scheduler', () => { log = []; // for doublechecking, we flip it inside out, and assert on the outermost - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.act(() => { TestRenderer.create(); }); diff --git a/fixtures/dom/src/__tests__/wrong-act-test.js b/fixtures/dom/src/__tests__/wrong-act-test.js index 6c054efec1bf9..10df65ce6df60 100644 --- a/fixtures/dom/src/__tests__/wrong-act-test.js +++ b/fixtures/dom/src/__tests__/wrong-act-test.js @@ -12,13 +12,17 @@ let ReactDOM; let ReactART; let ARTSVGMode; let ARTCurrentMode; -let TestUtils; let TestRenderer; let ARTTest; global.__DEV__ = process.env.NODE_ENV !== 'production'; global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental'; +jest.mock('react-dom', () => + require.requireActual('react-dom/cjs/react-dom-testing.development.js') +); +// we'll replace the above with react/testing and react-dom/testing right before the next minor + expect.extend(require('../toWarnDev')); function App(props) { @@ -32,7 +36,6 @@ beforeEach(() => { ReactART = require('react-art'); ARTSVGMode = require('art/modes/svg'); ARTCurrentMode = require('art/modes/current'); - TestUtils = require('react-dom/test-utils'); TestRenderer = require('react-test-renderer'); ARTCurrentMode.setCurrent(ARTSVGMode); @@ -70,8 +73,8 @@ beforeEach(() => { }); it("doesn't warn when you use the right act + renderer: dom", () => { - TestUtils.act(() => { - TestUtils.renderIntoDocument(); + ReactDOM.act(() => { + ReactDOM.render(, document.createElement('div')); }); }); @@ -86,7 +89,7 @@ it('resets correctly across renderers', () => { React.useEffect(() => {}, []); return null; } - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.act(() => {}); expect(() => { TestRenderer.create(); @@ -99,7 +102,7 @@ it('resets correctly across renderers', () => { it('warns when using the wrong act version - test + dom: render', () => { expect(() => { TestRenderer.act(() => { - TestUtils.renderIntoDocument(); + ReactDOM.render(, document.createElement('div')); }); }).toWarnDev(["It looks like you're using the wrong act()"], { withoutStack: true, @@ -113,7 +116,7 @@ it('warns when using the wrong act version - test + dom: updates', () => { setCtr = _setCtr; return ctr; } - TestUtils.renderIntoDocument(); + ReactDOM.render(, document.createElement('div')); expect(() => { TestRenderer.act(() => { setCtr(1); @@ -123,7 +126,7 @@ it('warns when using the wrong act version - test + dom: updates', () => { it('warns when using the wrong act version - dom + test: .create()', () => { expect(() => { - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.create(); }); }).toWarnDev(["It looks like you're using the wrong act()"], { @@ -134,7 +137,7 @@ it('warns when using the wrong act version - dom + test: .create()', () => { it('warns when using the wrong act version - dom + test: .update()', () => { const root = TestRenderer.create(); expect(() => { - TestUtils.act(() => { + ReactDOM.act(() => { root.update(); }); }).toWarnDev(["It looks like you're using the wrong act()"], { @@ -151,15 +154,15 @@ it('warns when using the wrong act version - dom + test: updates', () => { } TestRenderer.create(); expect(() => { - TestUtils.act(() => { + ReactDOM.act(() => { setCtr(1); }); }).toWarnDev(["It looks like you're using the wrong act()"]); }); it('does not warn when nesting react-act inside react-dom', () => { - TestUtils.act(() => { - TestUtils.renderIntoDocument(); + ReactDOM.act(() => { + ReactDOM.render(, document.createElement('div')); }); }); @@ -171,7 +174,7 @@ it('does not warn when nesting react-act inside react-test-renderer', () => { it("doesn't warn if you use nested acts from different renderers", () => { TestRenderer.act(() => { - TestUtils.act(() => { + ReactDOM.act(() => { TestRenderer.create(); }); }); diff --git a/packages/react-dom/npm/testing.js b/packages/react-dom/npm/testing.js new file mode 100644 index 0000000000000..0cb587bf9a9dc --- /dev/null +++ b/packages/react-dom/npm/testing.js @@ -0,0 +1,38 @@ +'use strict'; + +function checkDCE() { + /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */ + if ( + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined' || + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE !== 'function' + ) { + return; + } + if (process.env.NODE_ENV !== 'production') { + // This branch is unreachable because this function is only called + // in production, but the condition is true only in development. + // Therefore if the branch is still here, dead code elimination wasn't + // properly applied. + // Don't change the message. React DevTools relies on it. Also make sure + // this message doesn't occur elsewhere in this function, or it will cause + // a false positive. + throw new Error('^_^'); + } + try { + // Verify that the code above has been dead code eliminated (DCE'd). + __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); + } catch (err) { + // DevTools shouldn't crash React, no matter what. + // We should still report in case we break this code. + console.error(err); + } +} + +if (process.env.NODE_ENV === 'production') { + // DCE check should happen before ReactDOM bundle executes so that + // DevTools can report bad minification during injection. + checkDCE(); + module.exports = require('./cjs/react-dom-testing.production.min.js'); +} else { + module.exports = require('./cjs/react-dom-testing.development.js'); +} diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index d68a4a1141709..2f14259735000 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -722,6 +722,7 @@ function runActTests(label, render, unmount, rerender) { describe('suspense', () => { if (__DEV__ && __EXPERIMENTAL__) { + // todo - remove __DEV__ check once we start using testing builds it('triggers fallbacks if available', async () => { let resolved = false; let resolve; diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 51b4071363b01..1a1bf445f56a2 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -35,6 +35,7 @@ import { attemptUserBlockingHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, + act, } from 'react-reconciler/inline.dom'; import {createPortal as createPortalImpl} from 'shared/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -58,6 +59,7 @@ import { disableUnstableCreatePortal, disableUnstableRenderSubtreeIntoContainer, warnUnstableRenderSubtreeIntoContainer, + isTestEnvironment, } from 'shared/ReactFeatureFlags'; import { @@ -249,4 +251,8 @@ if (__DEV__) { } } +if (isTestEnvironment) { + ReactDOM.act = act; +} + export default ReactDOM; diff --git a/packages/react-dom/testing.js b/packages/react-dom/testing.js new file mode 100644 index 0000000000000..2a016ba16e9db --- /dev/null +++ b/packages/react-dom/testing.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +const ReactDOM = require('./src/client/ReactDOM'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactDOM.default || ReactDOM; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 5c11164ae66c4..87f72814ac96a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -14,7 +14,6 @@ * environment. */ -import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -23,8 +22,6 @@ import type {RootTag} from 'shared/ReactRootTags'; import * as Scheduler from 'scheduler/unstable_mock'; import {createPortal} from 'shared/ReactPortal'; import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; -import enqueueTask from 'shared/enqueueTask'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; import {ConcurrentRoot, BlockingRoot, LegacyRoot} from 'shared/ReactRootTags'; type Container = { @@ -60,8 +57,6 @@ type TextInstance = {| |}; type HostContext = Object; -const {IsSomeRendererActing} = ReactSharedInternals; - const NO_CONTEXT = {}; const UPPERCASE_CONTEXT = {}; const UPDATE_SIGNAL = {}; @@ -597,197 +592,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { const roots = new Map(); const DEFAULT_ROOT_ID = ''; - const { - flushPassiveEffects, - batchedUpdates, - IsThisRendererActing, - } = NoopRenderer; - - // this act() implementation should be exactly the same in - // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js - - const isSchedulerMocked = - typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; - const flushWork = - Scheduler.unstable_flushAllWithoutAsserting || - function() { - let didFlushWork = false; - while (flushPassiveEffects()) { - didFlushWork = true; - } - - return didFlushWork; - }; - - function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { - try { - flushWork(); - enqueueTask(() => { - if (flushWork()) { - flushWorkAndMicroTasks(onDone); - } else { - onDone(); - } - }); - } catch (err) { - onDone(err); - } - } - - // we track the 'depth' of the act() calls with this counter, - // so we can tell if any async act() calls try to run in parallel. - - let actingUpdatesScopeDepth = 0; - let didWarnAboutUsingActInProd = false; - - function act(callback: () => Thenable) { - if (!__DEV__) { - if (didWarnAboutUsingActInProd === false) { - didWarnAboutUsingActInProd = true; - // eslint-disable-next-line react-internal/no-production-logging - console.error( - 'act(...) is not supported in production builds of React, and might not behave as expected.', - ); - } - } - let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; - let previousIsSomeRendererActing; - let previousIsThisRendererActing; - actingUpdatesScopeDepth++; - - previousIsSomeRendererActing = IsSomeRendererActing.current; - previousIsThisRendererActing = IsThisRendererActing.current; - IsSomeRendererActing.current = true; - IsThisRendererActing.current = true; - - function onDone() { - actingUpdatesScopeDepth--; - IsSomeRendererActing.current = previousIsSomeRendererActing; - IsThisRendererActing.current = previousIsThisRendererActing; - if (__DEV__) { - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned - console.error( - 'You seem to have overlapping act() calls, this is not supported. ' + - 'Be sure to await previous act() calls before making a new one. ', - ); - } - } - } - - let result; - try { - result = batchedUpdates(callback); - } catch (error) { - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth - onDone(); - throw error; - } - - if ( - result !== null && - typeof result === 'object' && - typeof result.then === 'function' - ) { - // setup a boolean that gets set to true only - // once this act() call is await-ed - let called = false; - if (__DEV__) { - if (typeof Promise !== 'undefined') { - //eslint-disable-next-line no-undef - Promise.resolve() - .then(() => {}) - .then(() => { - if (called === false) { - console.error( - 'You called act(async () => ...) without await. ' + - 'This could lead to unexpected testing behaviour, interleaving multiple act ' + - 'calls and mixing their scopes. You should - await act(async () => ...);', - ); - } - }); - } - } - - // in the async case, the returned thenable runs the callback, flushes - // effects and microtasks in a loop until flushPassiveEffects() === false, - // and cleans up - return { - then(resolve: () => void, reject: (?Error) => void) { - called = true; - result.then( - () => { - if ( - actingUpdatesScopeDepth > 1 || - (isSchedulerMocked === true && - previousIsSomeRendererActing === true) - ) { - onDone(); - resolve(); - return; - } - // we're about to exit the act() scope, - // now's the time to flush tasks/effects - flushWorkAndMicroTasks((err: ?Error) => { - onDone(); - if (err) { - reject(err); - } else { - resolve(); - } - }); - }, - err => { - onDone(); - reject(err); - }, - ); - }, - }; - } else { - if (__DEV__) { - if (result !== undefined) { - console.error( - 'The callback passed to act(...) function ' + - 'must return undefined, or a Promise. You returned %s', - result, - ); - } - } - - // flush effects until none remain, and cleanup - try { - if ( - actingUpdatesScopeDepth === 1 && - (isSchedulerMocked === false || - previousIsSomeRendererActing === false) - ) { - // we're about to exit the act() scope, - // now's the time to flush effects - flushWork(); - } - onDone(); - } catch (err) { - onDone(); - throw err; - } - - // in the sync case, the returned thenable only warns *if* await-ed - return { - then(resolve: () => void) { - if (__DEV__) { - console.error( - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', - ); - } - resolve(); - }, - }; - } - } - - // end act() implementation - function childToJSX(child, text) { if (text !== null) { return text; @@ -1106,7 +910,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { flushPassiveEffects: NoopRenderer.flushPassiveEffects, - act, + act: NoopRenderer.act, // Logs the current state of the tree. dumpTree(rootID: string = DEFAULT_ROOT_ID) { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index fb46524645aee..2c2d2e7c8fe99 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -87,6 +87,11 @@ import { findHostInstancesForRefresh, } from './ReactFiberHotReloading'; +// used by isTestEnvironment builds +import enqueueTask from 'shared/enqueueTask'; +import * as Scheduler from 'scheduler'; +// end isTestEnvironment imports + type OpaqueRoot = FiberRoot; // 0 is PROD, 1 is DEV. @@ -538,3 +543,185 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { getCurrentFiber: __DEV__ ? () => ReactCurrentFiberCurrent : null, }); } + +const {IsSomeRendererActing} = ReactSharedInternals; +const isSchedulerMocked = + typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; +const flushWork = + Scheduler.unstable_flushAllWithoutAsserting || + function() { + let didFlushWork = false; + while (flushPassiveEffects()) { + didFlushWork = true; + } + + return didFlushWork; + }; + +function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { + try { + flushWork(); + enqueueTask(() => { + if (flushWork()) { + flushWorkAndMicroTasks(onDone); + } else { + onDone(); + } + }); + } catch (err) { + onDone(err); + } +} + +// we track the 'depth' of the act() calls with this counter, +// so we can tell if any async act() calls try to run in parallel. + +let actingUpdatesScopeDepth = 0; +let didWarnAboutUsingActInProd = false; + +// eslint-disable-next-line no-inner-declarations +export function act(callback: () => Thenable) { + if (!__DEV__) { + if (didWarnAboutUsingActInProd === false) { + didWarnAboutUsingActInProd = true; + // eslint-disable-next-line react-internal/no-production-logging + console.error( + 'act(...) is not supported in production builds of React, and might not behave as expected.', + ); + } + } + + let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; + let previousIsSomeRendererActing; + let previousIsThisRendererActing; + actingUpdatesScopeDepth++; + + previousIsSomeRendererActing = IsSomeRendererActing.current; + previousIsThisRendererActing = IsThisRendererActing.current; + IsSomeRendererActing.current = true; + IsThisRendererActing.current = true; + + function onDone() { + actingUpdatesScopeDepth--; + IsSomeRendererActing.current = previousIsSomeRendererActing; + IsThisRendererActing.current = previousIsThisRendererActing; + if (__DEV__) { + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned + console.error( + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + } + } + + let result; + try { + result = batchedUpdates(callback); + } catch (error) { + // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth + onDone(); + throw error; + } + + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // setup a boolean that gets set to true only + // once this act() call is await-ed + let called = false; + if (__DEV__) { + if (typeof Promise !== 'undefined') { + //eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (called === false) { + console.error( + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + + 'calls and mixing their scopes. You should - await act(async () => ...);', + ); + } + }); + } + } + + // in the async case, the returned thenable runs the callback, flushes + // effects and microtasks in a loop until flushPassiveEffects() === false, + // and cleans up + return { + then(resolve: () => void, reject: (?Error) => void) { + called = true; + result.then( + () => { + if ( + actingUpdatesScopeDepth > 1 || + (isSchedulerMocked === true && + previousIsSomeRendererActing === true) + ) { + onDone(); + resolve(); + return; + } + // we're about to exit the act() scope, + // now's the time to flush tasks/effects + flushWorkAndMicroTasks((err: ?Error) => { + onDone(); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, + err => { + onDone(); + reject(err); + }, + ); + }, + }; + } else { + if (__DEV__) { + if (result !== undefined) { + console.error( + 'The callback passed to act(...) function ' + + 'must return undefined, or a Promise. You returned %s', + result, + ); + } + } + + // flush effects until none remain, and cleanup + try { + if ( + actingUpdatesScopeDepth === 1 && + (isSchedulerMocked === false || previousIsSomeRendererActing === false) + ) { + // we're about to exit the act() scope, + // now's the time to flush effects + flushWork(); + } + onDone(); + } catch (err) { + onDone(); + throw err; + } + + // in the sync case, the returned thenable only warns *if* await-ed + return { + then(resolve: () => void) { + if (__DEV__) { + console.error( + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ); + } + resolve(); + }, + }; + } +} diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index ca56ffaaa2685..e8ec6cbd53e6a 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -19,6 +19,7 @@ import { flushSync, injectIntoDevTools, batchedUpdates, + act, } from 'react-reconciler/inline.test'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; import { @@ -42,7 +43,6 @@ import { } from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; import ReactVersion from 'shared/ReactVersion'; -import act from './ReactTestRendererAct'; import {getPublicInstance} from './ReactTestHostConfig'; import {ConcurrentRoot, LegacyRoot} from 'shared/ReactRootTags'; diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js deleted file mode 100644 index 362de4e2b22ae..0000000000000 --- a/packages/react-test-renderer/src/ReactTestRendererAct.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ -import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop'; - -import { - batchedUpdates, - flushPassiveEffects, - IsThisRendererActing, -} from 'react-reconciler/inline.test'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import enqueueTask from 'shared/enqueueTask'; -import * as Scheduler from 'scheduler'; - -const {IsSomeRendererActing} = ReactSharedInternals; - -// this implementation should be exactly the same in -// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js - -const isSchedulerMocked = - typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; -const flushWork = - Scheduler.unstable_flushAllWithoutAsserting || - function() { - let didFlushWork = false; - while (flushPassiveEffects()) { - didFlushWork = true; - } - - return didFlushWork; - }; - -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { - try { - flushWork(); - enqueueTask(() => { - if (flushWork()) { - flushWorkAndMicroTasks(onDone); - } else { - onDone(); - } - }); - } catch (err) { - onDone(err); - } -} - -// we track the 'depth' of the act() calls with this counter, -// so we can tell if any async act() calls try to run in parallel. - -let actingUpdatesScopeDepth = 0; -let didWarnAboutUsingActInProd = false; - -function act(callback: () => Thenable) { - if (!__DEV__) { - if (didWarnAboutUsingActInProd === false) { - didWarnAboutUsingActInProd = true; - // eslint-disable-next-line react-internal/no-production-logging - console.error( - 'act(...) is not supported in production builds of React, and might not behave as expected.', - ); - } - } - let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; - let previousIsSomeRendererActing; - let previousIsThisRendererActing; - actingUpdatesScopeDepth++; - - previousIsSomeRendererActing = IsSomeRendererActing.current; - previousIsThisRendererActing = IsThisRendererActing.current; - IsSomeRendererActing.current = true; - IsThisRendererActing.current = true; - - function onDone() { - actingUpdatesScopeDepth--; - IsSomeRendererActing.current = previousIsSomeRendererActing; - IsThisRendererActing.current = previousIsThisRendererActing; - if (__DEV__) { - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned - console.error( - 'You seem to have overlapping act() calls, this is not supported. ' + - 'Be sure to await previous act() calls before making a new one. ', - ); - } - } - } - - let result; - try { - result = batchedUpdates(callback); - } catch (error) { - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth - onDone(); - throw error; - } - - if ( - result !== null && - typeof result === 'object' && - typeof result.then === 'function' - ) { - // setup a boolean that gets set to true only - // once this act() call is await-ed - let called = false; - if (__DEV__) { - if (typeof Promise !== 'undefined') { - //eslint-disable-next-line no-undef - Promise.resolve() - .then(() => {}) - .then(() => { - if (called === false) { - console.error( - 'You called act(async () => ...) without await. ' + - 'This could lead to unexpected testing behaviour, interleaving multiple act ' + - 'calls and mixing their scopes. You should - await act(async () => ...);', - ); - } - }); - } - } - - // in the async case, the returned thenable runs the callback, flushes - // effects and microtasks in a loop until flushPassiveEffects() === false, - // and cleans up - return { - then(resolve: () => void, reject: (?Error) => void) { - called = true; - result.then( - () => { - if ( - actingUpdatesScopeDepth > 1 || - (isSchedulerMocked === true && - previousIsSomeRendererActing === true) - ) { - onDone(); - resolve(); - return; - } - // we're about to exit the act() scope, - // now's the time to flush tasks/effects - flushWorkAndMicroTasks((err: ?Error) => { - onDone(); - if (err) { - reject(err); - } else { - resolve(); - } - }); - }, - err => { - onDone(); - reject(err); - }, - ); - }, - }; - } else { - if (__DEV__) { - if (result !== undefined) { - console.error( - 'The callback passed to act(...) function ' + - 'must return undefined, or a Promise. You returned %s', - result, - ); - } - } - - // flush effects until none remain, and cleanup - try { - if ( - actingUpdatesScopeDepth === 1 && - (isSchedulerMocked === false || previousIsSomeRendererActing === false) - ) { - // we're about to exit the act() scope, - // now's the time to flush effects - flushWork(); - } - onDone(); - } catch (err) { - onDone(); - throw err; - } - - // in the sync case, the returned thenable only warns *if* await-ed - return { - then(resolve: () => void) { - if (__DEV__) { - console.error( - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', - ); - } - resolve(); - }, - }; - } -} - -export default act; diff --git a/packages/react/npm/testing.js b/packages/react/npm/testing.js new file mode 100644 index 0000000000000..f20bb1d19db68 --- /dev/null +++ b/packages/react/npm/testing.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-testing.production.min.js'); +} else { + module.exports = require('./cjs/react-testing.development.js'); +} diff --git a/packages/react/testing.js b/packages/react/testing.js new file mode 100644 index 0000000000000..4268898c088b9 --- /dev/null +++ b/packages/react/testing.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +const React = require('./src/React'); + +// TODO: decide on the top-level export form. +// This is hacky but makes it work with both Rollup and Jest. +module.exports = React.default || React; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ead32bb435b40..63eafd9a07719 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -116,3 +116,5 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // Disables ReactDOM.unstable_createPortal export const disableUnstableCreatePortal = false; + +export const isTestEnvironment = false; diff --git a/packages/shared/enqueueTask.js b/packages/shared/enqueueTask.js index 5bf23c18f3a02..c6905c89696e0 100644 --- a/packages/shared/enqueueTask.js +++ b/packages/shared/enqueueTask.js @@ -8,37 +8,41 @@ */ let didWarnAboutMessageChannel = false; -let enqueueTask; -try { - // read require off the module object to get around the bundlers. - // we don't want them to detect a require and bundle a Node polyfill. - let requireString = ('require' + Math.random()).slice(0, 7); - let nodeRequire = module && module[requireString]; - // assuming we're in node, let's try to get node's - // version of setImmediate, bypassing fake timers if any. - enqueueTask = nodeRequire('timers').setImmediate; -} catch (_err) { - // we're in a browser - // we can't use regular timers because they may still be faked - // so we try MessageChannel+postMessage instead - enqueueTask = function(callback: () => void) { - if (__DEV__) { - if (didWarnAboutMessageChannel === false) { - didWarnAboutMessageChannel = true; - if (typeof MessageChannel === 'undefined') { - console.error( - 'This browser does not have a MessageChannel implementation, ' + - 'so enqueuing tasks via await act(async () => ...) will fail. ' + - 'Please file an issue at https://github.com/facebook/react/issues ' + - 'if you encounter this warning.', - ); +let enqueueTaskImpl = null; + +export default function enqueueTask(task: () => void) { + if (enqueueTaskImpl === null) { + try { + // read require off the module object to get around the bundlers. + // we don't want them to detect a require and bundle a Node polyfill. + let requireString = ('require' + Math.random()).slice(0, 7); + let nodeRequire = module && module[requireString]; + // assuming we're in node, let's try to get node's + // version of setImmediate, bypassing fake timers if any. + enqueueTaskImpl = nodeRequire('timers').setImmediate; + } catch (_err) { + // we're in a browser + // we can't use regular timers because they may still be faked + // so we try MessageChannel+postMessage instead + enqueueTaskImpl = function(callback: () => void) { + if (__DEV__) { + if (didWarnAboutMessageChannel === false) { + didWarnAboutMessageChannel = true; + if (typeof MessageChannel === 'undefined') { + console.error( + 'This browser does not have a MessageChannel implementation, ' + + 'so enqueuing tasks via await act(async () => ...) will fail. ' + + 'Please file an issue at https://github.com/facebook/react/issues ' + + 'if you encounter this warning.', + ); + } + } } - } + const channel = new MessageChannel(); + channel.port1.onmessage = callback; + channel.port2.postMessage(undefined); + }; } - const channel = new MessageChannel(); - channel.port1.onmessage = callback; - channel.port2.postMessage(undefined); - }; + } + return enqueueTaskImpl(task); } - -export default enqueueTask; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 7ae4ab0e4113d..326f16c7e6bc6 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -50,6 +50,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 241dad2cf7e3c..e968ecacf8048 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index a8bb6bf0aff56..385ddd9f829b4 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = false; // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index ae94c8f7f5e7b..1c842b106778e 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -45,6 +45,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = true; // this should probably *never* change // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 60448b0097b0e..2cbee322dc0bc 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -43,6 +43,7 @@ export const disableTextareaChildren = false; export const disableUnstableRenderSubtreeIntoContainer = false; export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = true; // this should probably *never* change // Only used in www builds. export function addUserTimingListener() { diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js new file mode 100644 index 0000000000000..9e8d68c31f91a --- /dev/null +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import invariant from 'shared/invariant'; + +import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; +import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persistent'; + +export const debugRenderPhaseSideEffectsForStrictMode = false; +export const enableUserTimingAPI = __DEV__; +export const warnAboutDeprecatedLifecycles = true; +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; +export const enableProfilerTimer = __PROFILE__; +export const enableSchedulerTracing = __PROFILE__; +export const enableSuspenseServerRenderer = false; +export const enableSelectiveHydration = false; +export const enableChunksAPI = false; +export const disableJavaScriptURLs = false; +export const disableInputAttributeSyncing = false; +export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; +export const warnAboutShorthandPropertyCollision = false; +export const enableSchedulerDebugging = false; +export const enableDeprecatedFlareAPI = false; +export const enableFundamentalAPI = false; +export const enableScopeAPI = false; +export const enableJSXTransformAPI = false; +export const warnAboutUnmockedScheduler = false; +export const flushSuspenseFallbacksInTests = true; +export const enableSuspenseCallback = false; +export const warnAboutDefaultPropsOnFunctionComponents = false; +export const warnAboutStringRefs = false; +export const disableLegacyContext = false; +export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrainModelFix = true; +export const enableTrustedTypesIntegration = false; +export const enableNativeTargetAsInstance = false; +export const disableCreateFactory = false; +export const disableTextareaChildren = false; +export const disableUnstableRenderSubtreeIntoContainer = false; +export const warnUnstableRenderSubtreeIntoContainer = false; +export const disableUnstableCreatePortal = false; +export const isTestEnvironment = true; + +// Only used in www builds. +export function addUserTimingListener() { + invariant(false, 'Not implemented.'); +} + +// Flow magic to verify the exports of this file match the original version. +// eslint-disable-next-line no-unused-vars +type Check<_X, Y: _X, X: Y = _X> = null; +// eslint-disable-next-line no-unused-expressions +(null: Check); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index cd3e59e5fe589..2aadf6536b160 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -101,6 +101,8 @@ export const warnUnstableRenderSubtreeIntoContainer = false; export const disableUnstableCreatePortal = false; +export const isTestEnvironment = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index aefbb65e4f1b1..fd262e49f401d 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -104,6 +104,31 @@ const bundles = [ externals: ['react', 'react-dom'], }, + /******* React DOM - Testing *******/ + { + moduleType: RENDERER, + bundleTypes: [ + UMD_DEV, + UMD_PROD, + UMD_PROFILING, + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + ], + entry: 'react-dom/testing', + global: 'ReactDOM', + externals: ['react'], + }, + + /******* React DOM - www - Testing *******/ + { + moduleType: RENDERER, + bundleTypes: [FB_WWW_DEV, FB_WWW_PROD, FB_WWW_PROFILING], + entry: 'react-dom/testing', + global: 'ReactDOMTesting', + externals: ['react'], + }, + /* React DOM internals required for react-native-web (e.g., to shim native events from react-dom) */ { bundleTypes: [ @@ -517,7 +542,7 @@ const bundles = [ externals: [], }, - /******* ESLint Plugin for Hooks (proposal) *******/ + /******* ESLint Plugin for Hooks *******/ { // TODO: it's awkward to create a bundle for this but if we don't, the package // won't get copied. We also can't create just DEV bundle because it contains a diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index eeb0c1caee8a7..9f1f3c41bdd62 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -1,23 +1,23 @@ 'use strict'; -const bundleTypes = require('./bundles').bundleTypes; -const moduleTypes = require('./bundles').moduleTypes; +const {bundleTypes, moduleTypes} = require('./bundles'); const inlinedHostConfigs = require('../shared/inlinedHostConfigs'); -const UMD_DEV = bundleTypes.UMD_DEV; -const UMD_PROD = bundleTypes.UMD_PROD; -const UMD_PROFILING = bundleTypes.UMD_PROFILING; -const FB_WWW_DEV = bundleTypes.FB_WWW_DEV; -const FB_WWW_PROD = bundleTypes.FB_WWW_PROD; -const FB_WWW_PROFILING = bundleTypes.FB_WWW_PROFILING; -const RN_OSS_DEV = bundleTypes.RN_OSS_DEV; -const RN_OSS_PROD = bundleTypes.RN_OSS_PROD; -const RN_OSS_PROFILING = bundleTypes.RN_OSS_PROFILING; -const RN_FB_DEV = bundleTypes.RN_FB_DEV; -const RN_FB_PROD = bundleTypes.RN_FB_PROD; -const RN_FB_PROFILING = bundleTypes.RN_FB_PROFILING; -const RENDERER = moduleTypes.RENDERER; -const RECONCILER = moduleTypes.RECONCILER; +const { + UMD_DEV, + UMD_PROD, + UMD_PROFILING, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + RN_OSS_DEV, + RN_OSS_PROD, + RN_OSS_PROFILING, + RN_FB_DEV, + RN_FB_PROD, + RN_FB_PROFILING, +} = bundleTypes; +const {RENDERER, RECONCILER} = moduleTypes; // If you need to replace a file with another file for a specific environment, // add it to this list with the logic for choosing the right replacement. @@ -46,7 +46,7 @@ const forks = Object.freeze({ // Without this fork, importing `shared/ReactSharedInternals` inside // the `react` package itself would not work due to a cyclical dependency. 'shared/ReactSharedInternals': (bundleType, entry, dependencies) => { - if (entry === 'react') { + if (entry === 'react' || entry === 'react/testing') { return 'react/src/ReactSharedInternals'; } if (dependencies.indexOf('react') === -1) { @@ -106,6 +106,10 @@ const forks = Object.freeze({ return 'shared/forks/ReactFeatureFlags.test-renderer.www.js'; } return 'shared/forks/ReactFeatureFlags.test-renderer.js'; + case 'react-dom/testing': + return 'shared/forks/ReactFeatureFlags.testing.js'; + case 'react/testing': + return 'shared/forks/ReactFeatureFlags.testing.js'; default: switch (bundleType) { case FB_WWW_DEV: diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index 968f98bc3f081..a5975f3d2e64b 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -1,11 +1,7 @@ 'use strict'; const forks = require('./forks'); -const bundleTypes = require('./bundles').bundleTypes; - -const UMD_DEV = bundleTypes.UMD_DEV; -const UMD_PROD = bundleTypes.UMD_PROD; -const UMD_PROFILING = bundleTypes.UMD_PROFILING; +const {UMD_DEV, UMD_PROD, UMD_PROFILING} = require('./bundles').bundleTypes; // For any external that is used in a DEV-only condition, explicitly // specify whether it has side effects during import or not. This lets diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index 7f17491936968..f9ca412c4364e 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -1,25 +1,27 @@ 'use strict'; -const Bundles = require('./bundles'); +const {bundleTypes, moduleTypes} = require('./bundles'); const reactVersion = require('../../package.json').version; -const UMD_DEV = Bundles.bundleTypes.UMD_DEV; -const UMD_PROD = Bundles.bundleTypes.UMD_PROD; -const UMD_PROFILING = Bundles.bundleTypes.UMD_PROFILING; -const NODE_DEV = Bundles.bundleTypes.NODE_DEV; -const NODE_PROD = Bundles.bundleTypes.NODE_PROD; -const NODE_PROFILING = Bundles.bundleTypes.NODE_PROFILING; -const FB_WWW_DEV = Bundles.bundleTypes.FB_WWW_DEV; -const FB_WWW_PROD = Bundles.bundleTypes.FB_WWW_PROD; -const FB_WWW_PROFILING = Bundles.bundleTypes.FB_WWW_PROFILING; -const RN_OSS_DEV = Bundles.bundleTypes.RN_OSS_DEV; -const RN_OSS_PROD = Bundles.bundleTypes.RN_OSS_PROD; -const RN_OSS_PROFILING = Bundles.bundleTypes.RN_OSS_PROFILING; -const RN_FB_DEV = Bundles.bundleTypes.RN_FB_DEV; -const RN_FB_PROD = Bundles.bundleTypes.RN_FB_PROD; -const RN_FB_PROFILING = Bundles.bundleTypes.RN_FB_PROFILING; - -const RECONCILER = Bundles.moduleTypes.RECONCILER; +const { + UMD_DEV, + UMD_PROD, + UMD_PROFILING, + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + RN_OSS_DEV, + RN_OSS_PROD, + RN_OSS_PROFILING, + RN_FB_DEV, + RN_FB_PROD, + RN_FB_PROFILING, +} = bundleTypes; + +const {RECONCILER} = moduleTypes; const license = ` * Copyright (c) Facebook, Inc. and its affiliates. * diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js index 81814324e6c48..c92deb2f24f37 100644 --- a/scripts/shared/inlinedHostConfigs.js +++ b/scripts/shared/inlinedHostConfigs.js @@ -11,6 +11,7 @@ module.exports = [ shortName: 'dom', entryPoints: [ 'react-dom', + 'react-dom/testing', 'react-dom/unstable-fizz.node', 'react-flight-dom-webpack/server.node', 'react-flight-dom-webpack',