From e6a0473c3c6f501dbe291f60b9ee35760ab99eed Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Tue, 30 Jul 2019 19:00:18 +0100 Subject: [PATCH] Warn when rendering tests in concurrent/batched mode without a mocked scheduler (#16207) Concurrent/Batched mode tests should always be run with a mocked scheduler (v17 or not). This PR adds a warning for the same. I'll put up a separate PR to the docs with a page detailing how to mock the scheduler. --- fixtures/dom/src/__tests__/nested-act-test.js | 97 ++++++++ fixtures/dom/src/__tests__/wrong-act-test.js | 194 +++++++++++++++ fixtures/dom/src/index.test.js | 233 ------------------ .../ReactTestUtilsAct-test.internal.js | 28 --- ...tUnmockedSchedulerWarning-test.internal.js | 42 ++++ .../ReactUnmockedSchedulerWarning-test.js | 60 +++++ .../src/test-utils/ReactTestUtilsAct.js | 17 -- .../src/createReactNoop.js | 17 -- .../src/ReactFiberReconciler.js | 2 + .../src/ReactFiberWorkLoop.js | 39 +++ .../src/ReactTestRendererAct.js | 17 -- packages/shared/ReactFeatureFlags.js | 2 +- .../forks/ReactFeatureFlags.native-fb.js | 2 +- .../forks/ReactFeatureFlags.native-oss.js | 2 +- .../forks/ReactFeatureFlags.persistent.js | 2 +- .../forks/ReactFeatureFlags.test-renderer.js | 2 +- .../ReactFeatureFlags.test-renderer.www.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 2 +- 18 files changed, 441 insertions(+), 319 deletions(-) create mode 100644 fixtures/dom/src/__tests__/nested-act-test.js create mode 100644 fixtures/dom/src/__tests__/wrong-act-test.js delete mode 100644 fixtures/dom/src/index.test.js delete mode 100644 packages/react-dom/src/__tests__/ReactTestUtilsAct-test.internal.js create mode 100644 packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js create mode 100644 packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js new file mode 100644 index 0000000000000..a0500f47e9603 --- /dev/null +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -0,0 +1,97 @@ +/** + * 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. + * + * @emails react-core + */ + +let React; +let TestUtils; +let TestRenderer; + +global.__DEV__ = process.env.NODE_ENV !== 'production'; + +expect.extend(require('../toWarnDev')); + +describe('unmocked scheduler', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + TestUtils = require('react-dom/test-utils'); + TestRenderer = require('react-test-renderer'); + }); + + it('flushes work only outside the outermost act() corresponding to its own renderer', () => { + let log = []; + function Effecty() { + React.useEffect(() => { + log.push('called'); + }, []); + return null; + } + // in legacy mode, this tests whether an act only flushes its own effects + TestRenderer.act(() => { + TestUtils.act(() => { + TestRenderer.create(); + }); + expect(log).toEqual([]); + }); + expect(log).toEqual(['called']); + + log = []; + // for doublechecking, we flip it inside out, and assert on the outermost + TestUtils.act(() => { + TestRenderer.act(() => { + TestRenderer.create(); + }); + expect(log).toEqual(['called']); + }); + expect(log).toEqual(['called']); + }); +}); + +describe('mocked scheduler', () => { + beforeEach(() => { + jest.resetModules(); + jest.mock('scheduler', () => + require.requireActual('scheduler/unstable_mock') + ); + React = require('react'); + TestUtils = require('react-dom/test-utils'); + TestRenderer = require('react-test-renderer'); + }); + + afterEach(() => { + jest.unmock('scheduler'); + }); + + it('flushes work only outside the outermost act()', () => { + let log = []; + function Effecty() { + React.useEffect(() => { + log.push('called'); + }, []); + return null; + } + // with a mocked scheduler, this tests whether it flushes all work only on the outermost act + TestRenderer.act(() => { + TestUtils.act(() => { + TestRenderer.create(); + }); + expect(log).toEqual([]); + }); + expect(log).toEqual(['called']); + + log = []; + // for doublechecking, we flip it inside out, and assert on the outermost + TestUtils.act(() => { + TestRenderer.act(() => { + TestRenderer.create(); + }); + expect(log).toEqual([]); + }); + expect(log).toEqual(['called']); + }); +}); diff --git a/fixtures/dom/src/__tests__/wrong-act-test.js b/fixtures/dom/src/__tests__/wrong-act-test.js new file mode 100644 index 0000000000000..05dda494d1126 --- /dev/null +++ b/fixtures/dom/src/__tests__/wrong-act-test.js @@ -0,0 +1,194 @@ +/** + * 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. + * + * @emails react-core + */ + +let React; +let ReactDOM; +let ReactART; +let ARTSVGMode; +let ARTCurrentMode; +let TestUtils; +let TestRenderer; +let ARTTest; + +global.__DEV__ = process.env.NODE_ENV !== 'production'; + +expect.extend(require('../toWarnDev')); + +function App(props) { + return 'hello world'; +} + +beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + 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); + + ARTTest = function ARTTestComponent(props) { + return ( + + + + + M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666 + h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994 + h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z + + + + ); + }; +}); + +it("doesn't warn when you use the right act + renderer: dom", () => { + TestUtils.act(() => { + TestUtils.renderIntoDocument(); + }); +}); + +it("doesn't warn when you use the right act + renderer: test", () => { + TestRenderer.act(() => { + TestRenderer.create(); + }); +}); + +it('resets correctly across renderers', () => { + function Effecty() { + React.useEffect(() => {}, []); + return null; + } + TestUtils.act(() => { + TestRenderer.act(() => {}); + expect(() => { + TestRenderer.create(); + }).toWarnDev(["It looks like you're using the wrong act()"], { + withoutStack: true, + }); + }); +}); + +it('warns when using the wrong act version - test + dom: render', () => { + expect(() => { + TestRenderer.act(() => { + TestUtils.renderIntoDocument(); + }); + }).toWarnDev(["It looks like you're using the wrong act()"], { + withoutStack: true, + }); +}); + +it('warns when using the wrong act version - test + dom: updates', () => { + let setCtr; + function Counter(props) { + const [ctr, _setCtr] = React.useState(0); + setCtr = _setCtr; + return ctr; + } + TestUtils.renderIntoDocument(); + expect(() => { + TestRenderer.act(() => { + setCtr(1); + }); + }).toWarnDev(["It looks like you're using the wrong act()"]); +}); + +it('warns when using the wrong act version - dom + test: .create()', () => { + expect(() => { + TestUtils.act(() => { + TestRenderer.create(); + }); + }).toWarnDev(["It looks like you're using the wrong act()"], { + withoutStack: true, + }); +}); + +it('warns when using the wrong act version - dom + test: .update()', () => { + const root = TestRenderer.create(); + expect(() => { + TestUtils.act(() => { + root.update(); + }); + }).toWarnDev(["It looks like you're using the wrong act()"], { + withoutStack: true, + }); +}); + +it('warns when using the wrong act version - dom + test: updates', () => { + let setCtr; + function Counter(props) { + const [ctr, _setCtr] = React.useState(0); + setCtr = _setCtr; + return ctr; + } + TestRenderer.create(); + expect(() => { + TestUtils.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(); + }); +}); + +it('does not warn when nesting react-act inside react-test-renderer', () => { + TestRenderer.act(() => { + TestRenderer.create(); + }); +}); + +it("doesn't warn if you use nested acts from different renderers", () => { + TestRenderer.act(() => { + TestUtils.act(() => { + TestRenderer.create(); + }); + }); +}); + +it('warns when using createRoot() + .render', () => { + const root = ReactDOM.unstable_createRoot(document.createElement('div')); + expect(() => { + TestRenderer.act(() => { + root.render(); + }); + }).toWarnDev( + [ + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked', + "It looks like you're using the wrong act()", + ], + { + withoutStack: true, + } + ); +}); diff --git a/fixtures/dom/src/index.test.js b/fixtures/dom/src/index.test.js deleted file mode 100644 index e562fd61c845b..0000000000000 --- a/fixtures/dom/src/index.test.js +++ /dev/null @@ -1,233 +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. - * - * @emails react-core - */ - -let React; -let ReactDOM; -let ReactART; -let ARTSVGMode; -let ARTCurrentMode; -let TestUtils; -let TestRenderer; -let ARTTest; - -global.__DEV__ = process.env.NODE_ENV !== 'production'; - -expect.extend(require('./toWarnDev')); - -function App(props) { - return 'hello world'; -} - -describe('legacy mode', () => { - runTests(); -}); - -describe('mocked scheduler', () => { - beforeEach(() => { - jest.mock('scheduler', () => - require.requireActual('scheduler/unstable_mock') - ); - }); - afterEach(() => { - jest.unmock('scheduler'); - }); - runTests(); -}); - -function runTests() { - beforeEach(() => { - jest.resetModules(); - React = require('react'); - ReactDOM = require('react-dom'); - 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); - - ARTTest = function ARTTest(props) { - return ( - - - - - M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666 - h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994 - h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z - - - - ); - }; - }); - it("doesn't warn when you use the right act + renderer: dom", () => { - TestUtils.act(() => { - TestUtils.renderIntoDocument(); - }); - }); - - it("doesn't warn when you use the right act + renderer: test", () => { - TestRenderer.act(() => { - TestRenderer.create(); - }); - }); - - it('resets correctly across renderers', () => { - function Effecty() { - React.useEffect(() => {}, []); - return null; - } - TestUtils.act(() => { - TestRenderer.act(() => {}); - expect(() => { - TestRenderer.create(); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); - }); - - it('warns when using createRoot() + .render', () => { - const root = ReactDOM.unstable_createRoot(document.createElement('div')); - expect(() => { - TestRenderer.act(() => { - root.render(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); - - it('warns when using the wrong act version - test + dom: render', () => { - expect(() => { - TestRenderer.act(() => { - TestUtils.renderIntoDocument(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); - - it('warns when using the wrong act version - test + dom: updates', () => { - let setCtr; - function Counter(props) { - const [ctr, _setCtr] = React.useState(0); - setCtr = _setCtr; - return ctr; - } - TestUtils.renderIntoDocument(); - expect(() => { - TestRenderer.act(() => { - setCtr(1); - }); - }).toWarnDev(["It looks like you're using the wrong act()"]); - }); - - it('warns when using the wrong act version - dom + test: .create()', () => { - expect(() => { - TestUtils.act(() => { - TestRenderer.create(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); - - it('warns when using the wrong act version - dom + test: .update()', () => { - const root = TestRenderer.create(); - expect(() => { - TestUtils.act(() => { - root.update(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); - - it('warns when using the wrong act version - dom + test: updates', () => { - let setCtr; - function Counter(props) { - const [ctr, _setCtr] = React.useState(0); - setCtr = _setCtr; - return ctr; - } - const root = TestRenderer.create(); - expect(() => { - TestUtils.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(); - }); - }); - - it('does not warn when nesting react-act inside react-test-renderer', () => { - TestRenderer.act(() => { - TestRenderer.create(); - }); - }); - - it("doesn't warn if you use nested acts from different renderers", () => { - TestRenderer.act(() => { - TestUtils.act(() => { - TestRenderer.create(); - }); - }); - }); - - it('flushes work only outside the outermost act(), even when nested from different renderers', () => { - const log = []; - function Effecty() { - React.useEffect(() => { - log.push('called'); - }, []); - return null; - } - // in legacy mode, this tests whether an act only flushes its own effects - // with a mocked scheduler, this tests whether it flushes all work only on the outermost act - TestRenderer.act(() => { - TestUtils.act(() => { - TestRenderer.create(); - }); - expect(log).toEqual([]); - }); - expect(log).toEqual(['called']); - - log.splice(0); - // for doublechecking, we flip it inside out, and assert on the outermost - TestUtils.act(() => { - TestRenderer.act(() => { - TestRenderer.create(); - }); - }); - expect(log).toEqual(['called']); - }); -} diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.internal.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.internal.js deleted file mode 100644 index d629795c0727e..0000000000000 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.internal.js +++ /dev/null @@ -1,28 +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. - * - * @emails react-core - */ - -let ReactFeatureFlags; -let act; -describe('mocked scheduler', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.warnAboutMissingMockScheduler = true; - jest.unmock('scheduler'); - act = require('react-dom/test-utils').act; - }); - it("should warn when the scheduler isn't mocked", () => { - expect(() => act(() => {})).toWarnDev( - [ - 'Starting from React v17, the "scheduler" module will need to be mocked', - ], - {withoutStack: true}, - ); - }); -}); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js new file mode 100644 index 0000000000000..1543d9b97c3e8 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js @@ -0,0 +1,42 @@ +/** + * 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. + * + * @emails react-core + */ + +let React; +let ReactDOM; +let ReactFeatureFlags; + +function App() { + return null; +} + +beforeEach(() => { + jest.resetModules(); + jest.unmock('scheduler'); + React = require('react'); + ReactDOM = require('react-dom'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.warnAboutUnmockedScheduler = true; +}); + +afterEach(() => { + ReactFeatureFlags.warnAboutUnmockedScheduler = false; +}); + +it('should warn in sync mode', () => { + expect(() => { + ReactDOM.render(, document.createElement('div')); + }).toWarnDev( + ['Starting from React v17, the "scheduler" module will need to be mocked'], + {withoutStack: true}, + ); + // does not warn twice + expect(() => { + ReactDOM.render(, document.createElement('div')); + }).toWarnDev([]); +}); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js new file mode 100644 index 0000000000000..ade05f823b64f --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js @@ -0,0 +1,60 @@ +/** + * 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. + * + * @emails react-core + */ + +let React; +let ReactDOM; + +function App() { + return null; +} + +beforeEach(() => { + jest.resetModules(); + jest.unmock('scheduler'); + React = require('react'); + ReactDOM = require('react-dom'); +}); + +it('does not warn when rendering in sync mode', () => { + expect(() => { + ReactDOM.render(, document.createElement('div')); + }).toWarnDev([]); +}); + +it('should warn when rendering in concurrent mode', () => { + expect(() => { + ReactDOM.unstable_createRoot(document.createElement('div')).render(); + }).toWarnDev( + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers.', + {withoutStack: true}, + ); + // does not warn twice + expect(() => { + ReactDOM.unstable_createRoot(document.createElement('div')).render(); + }).toWarnDev([]); +}); + +it('should warn when rendering in batched mode', () => { + expect(() => { + ReactDOM.unstable_createSyncRoot(document.createElement('div')).render( + , + ); + }).toWarnDev( + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers.', + {withoutStack: true}, + ); + // does not warn twice + expect(() => { + ReactDOM.unstable_createSyncRoot(document.createElement('div')).render( + , + ); + }).toWarnDev([]); +}); diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js index a0ccce2c9c9a4..f12915518bdd7 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js +++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js @@ -12,7 +12,6 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop'; import warningWithoutStack from 'shared/warningWithoutStack'; import ReactDOM from 'react-dom'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags'; import enqueueTask from 'shared/enqueueTask'; import * as Scheduler from 'scheduler'; @@ -43,27 +42,11 @@ const {IsSomeRendererActing} = ReactSharedInternals; // this implementation should be exactly the same in // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js -let hasWarnedAboutMissingMockScheduler = false; const isSchedulerMocked = typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; const flushWork = Scheduler.unstable_flushAllWithoutAsserting || function() { - if (warnAboutMissingMockScheduler === true) { - if (hasWarnedAboutMissingMockScheduler === false) { - warningWithoutStack( - null, - 'Starting from React v17, the "scheduler" module will need to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' + - "to the top of your tests, or in your framework's global config file -\n\n" + - 'As an example, for jest - \n' + - "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://fb.me/react-mock-scheduler', - ); - hasWarnedAboutMissingMockScheduler = true; - } - } - let didFlushWork = false; while (flushPassiveEffects()) { didFlushWork = true; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 451c3c4dccc88..42add1285cb7c 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -27,7 +27,6 @@ import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import enqueueTask from 'shared/enqueueTask'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags'; import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags'; type Container = { @@ -599,27 +598,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // this act() implementation should be exactly the same in // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js - let hasWarnedAboutMissingMockScheduler = false; const isSchedulerMocked = typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; const flushWork = Scheduler.unstable_flushAllWithoutAsserting || function() { - if (warnAboutMissingMockScheduler === true) { - if (hasWarnedAboutMissingMockScheduler === false) { - warningWithoutStack( - null, - 'Starting from React v17, the "scheduler" module will need to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' + - "to the top of your tests, or in your framework's global config file -\n\n" + - 'As an example, for jest - \n' + - "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://fb.me/react-mock-scheduler', - ); - hasWarnedAboutMissingMockScheduler = true; - } - } - let didFlushWork = false; while (flushPassiveEffects()) { didFlushWork = true; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index aade7ccb15d3b..6663699d8b700 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -58,6 +58,7 @@ import { flushDiscreteUpdates, flushPassiveEffects, warnIfNotScopedWithMatchingAct, + warnIfUnmockedScheduler, IsThisRendererActing, } from './ReactFiberWorkLoop'; import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; @@ -314,6 +315,7 @@ export function updateContainer( if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests if ('undefined' !== typeof jest) { + warnIfUnmockedScheduler(current); warnIfNotScopedWithMatchingAct(current); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index d4d4872450f12..53442df6e70ec 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -25,6 +25,7 @@ import { enableProfilerTimer, enableSchedulerTracing, revertPassiveEffectsChange, + warnAboutUnmockedScheduler, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -47,6 +48,9 @@ import { scheduleSyncCallback, } from './SchedulerWithReactIntegration'; +// The scheduler is imported here *only* to detect whether it's been mocked +import * as Scheduler from 'scheduler'; + import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; import { @@ -2522,6 +2526,41 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; +// In tests, we want to enforce a mocked scheduler. +let didWarnAboutUnmockedScheduler = false; +// TODO Before we release concurrent mode, revisit this and decide whether a mocked +// scheduler is the actual recommendation. The alternative could be a testing build, +// a new lib, or whatever; we dunno just yet. This message is for early adopters +// to get their tests right. + +export function warnIfUnmockedScheduler(fiber: Fiber) { + if (__DEV__) { + if (didWarnAboutUnmockedScheduler === false) { + if (fiber.mode & BatchedMode || fiber.mode & ConcurrentMode) { + didWarnAboutUnmockedScheduler = true; + warningWithoutStack( + Scheduler.unstable_flushAllWithoutAsserting !== undefined, + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers. ' + + 'For example, with jest: \n' + + "jest.mock('scheduler', () => require('scheduler/unstable_mock'));\n\n" + + 'For more info, visit https://fb.me/react-mock-scheduler', + ); + } else if (warnAboutUnmockedScheduler === true) { + didWarnAboutUnmockedScheduler = true; + warningWithoutStack( + null, + 'Starting from React v17, the "scheduler" module will need to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers. ' + + 'For example, with jest: \n' + + "jest.mock('scheduler', () => require('scheduler/unstable_mock'));\n\n" + + 'For more info, visit https://fb.me/react-mock-scheduler', + ); + } + } + } +} + let componentsThatSuspendedAtHighPri = null; let componentsThatTriggeredHighPriSuspend = null; export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) { diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js index 83408301a6994..726a62a15a277 100644 --- a/packages/react-test-renderer/src/ReactTestRendererAct.js +++ b/packages/react-test-renderer/src/ReactTestRendererAct.js @@ -15,7 +15,6 @@ import { } from 'react-reconciler/inline.test'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags'; import enqueueTask from 'shared/enqueueTask'; import * as Scheduler from 'scheduler'; @@ -24,27 +23,11 @@ const {IsSomeRendererActing} = ReactSharedInternals; // this implementation should be exactly the same in // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js -let hasWarnedAboutMissingMockScheduler = false; const isSchedulerMocked = typeof Scheduler.unstable_flushAllWithoutAsserting === 'function'; const flushWork = Scheduler.unstable_flushAllWithoutAsserting || function() { - if (warnAboutMissingMockScheduler === true) { - if (hasWarnedAboutMissingMockScheduler === false) { - warningWithoutStack( - null, - 'Starting from React v17, the "scheduler" module will need to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. To fix this, add the following ' + - "to the top of your tests, or in your framework's global config file -\n\n" + - 'As an example, for jest - \n' + - "jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://fb.me/react-mock-scheduler', - ); - hasWarnedAboutMissingMockScheduler = true; - } - } - let didFlushWork = false; while (flushPassiveEffects()) { didFlushWork = true; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ac92811f09541..083da1b75075a 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -70,7 +70,7 @@ export const enableJSXTransformAPI = false; // We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?) // Till then, we warn about the missing mock, but still fallback to a sync mode compatible version -export const warnAboutMissingMockScheduler = false; +export const warnAboutUnmockedScheduler = false; // Temporary flag to revert the fix in #15650 export const revertPassiveEffectsChange = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 08baee4c30ca5..427db540edc87 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -34,7 +34,7 @@ export const warnAboutDeprecatedSetNativeProps = true; export const enableFlareAPI = false; export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; -export const warnAboutMissingMockScheduler = true; +export const warnAboutUnmockedScheduler = true; export const revertPassiveEffectsChange = false; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index e1bb4ecae4bfd..cf69b2dc91cb7 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -29,7 +29,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; -export const warnAboutMissingMockScheduler = false; +export const warnAboutUnmockedScheduler = false; export const revertPassiveEffectsChange = false; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index c392336fc5188..7258b2ae1a7cb 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -29,7 +29,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; -export const warnAboutMissingMockScheduler = true; +export const warnAboutUnmockedScheduler = true; export const revertPassiveEffectsChange = false; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index a38d1f5ff8218..e9e16e4b3f7df 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -29,7 +29,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; -export const warnAboutMissingMockScheduler = false; +export const warnAboutUnmockedScheduler = false; export const revertPassiveEffectsChange = false; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index c8f1beb3e6288..86222406e391c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -30,7 +30,7 @@ export const disableJavaScriptURLs = false; export const enableFlareAPI = true; export const enableFundamentalAPI = false; export const enableJSXTransformAPI = true; -export const warnAboutMissingMockScheduler = true; +export const warnAboutUnmockedScheduler = true; export const enableUserBlockingEvents = false; export const enableSuspenseCallback = true; export const warnAboutDefaultPropsOnFunctionComponents = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 284416c8255b2..f0123e67b41ae 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -74,7 +74,7 @@ export const enableFundamentalAPI = false; export const enableJSXTransformAPI = true; -export const warnAboutMissingMockScheduler = true; +export const warnAboutUnmockedScheduler = true; export const enableSuspenseCallback = true;