From f161ee2eb7e78d6cb3d3878fe1812ac1057fedc6 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 21 Mar 2019 14:44:08 -0700 Subject: [PATCH] React.warn() and React.error() (#15170) --- packages/react/src/React.js | 4 + .../src/__tests__/withComponentStack-test.js | 192 ++++++++++++++++++ packages/react/src/withComponentStack.js | 48 +++++ 3 files changed, 244 insertions(+) create mode 100644 packages/react/src/__tests__/withComponentStack-test.js create mode 100644 packages/react/src/withComponentStack.js diff --git a/packages/react/src/React.js b/packages/react/src/React.js index a008fa96378c2..ba696073f82a5 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -45,6 +45,7 @@ import { cloneElementWithValidation, } from './ReactElementValidator'; import ReactSharedInternals from './ReactSharedInternals'; +import {error, warn} from './withComponentStack'; import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; const React = { @@ -65,6 +66,9 @@ const React = { lazy, memo, + error, + warn, + useCallback, useContext, useEffect, diff --git a/packages/react/src/__tests__/withComponentStack-test.js b/packages/react/src/__tests__/withComponentStack-test.js new file mode 100644 index 0000000000000..e46d7f2e54997 --- /dev/null +++ b/packages/react/src/__tests__/withComponentStack-test.js @@ -0,0 +1,192 @@ +/** + * 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 + */ + +'use strict'; + +function normalizeCodeLocInfo(str) { + return str && str.replace(/at .+?:\d+/g, 'at **'); +} + +function expectHelper(spy, prefix, ...expectedArgs) { + const expectedStack = expectedArgs.pop(); + + expect(spy).toHaveBeenCalledTimes(1); + + const actualArgs = spy.calls.mostRecent().args; + + let actualStack = undefined; + if (expectedStack !== undefined) { + actualStack = actualArgs.pop(); + expect(normalizeCodeLocInfo(actualStack)).toBe(expectedStack); + } + + expect(actualArgs).toHaveLength(expectedArgs.length); + actualArgs.forEach((actualArg, index) => { + const expectedArg = expectedArgs[index]; + expect(actualArg).toBe( + index === 0 ? `${prefix}: ${expectedArg}` : expectedArg, + ); + }); +} + +function expectMessageAndStack(...expectedArgs) { + expectHelper(console.error, 'error', ...expectedArgs); + expectHelper(console.warn, 'warn', ...expectedArgs); +} + +describe('withComponentStack', () => { + let React = null; + let ReactTestRenderer = null; + let error = null; + let scheduler = null; + let warn = null; + + beforeEach(() => { + jest.resetModules(); + jest.mock('scheduler', () => require('scheduler/unstable_mock')); + + React = require('react'); + ReactTestRenderer = require('react-test-renderer'); + scheduler = require('scheduler'); + + error = React.error; + warn = React.warn; + + spyOnDevAndProd(console, 'error'); + spyOnDevAndProd(console, 'warn'); + }); + + if (!__DEV__) { + it('does nothing in production mode', () => { + error('error'); + warn('warning'); + + expect(console.error).toHaveBeenCalledTimes(0); + expect(console.warn).toHaveBeenCalledTimes(0); + }); + } + + if (__DEV__) { + it('does not include component stack when called outside of render', () => { + error('error: logged outside of render'); + warn('warn: logged outside of render'); + expectMessageAndStack('logged outside of render', undefined); + }); + + it('should support multiple args', () => { + function Component() { + error('error: number:', 123, 'boolean:', true); + warn('warn: number:', 123, 'boolean:', true); + return null; + } + + ReactTestRenderer.create(); + + expectMessageAndStack( + 'number:', + 123, + 'boolean:', + true, + '\n in Component (at **)', + ); + }); + + it('includes component stack when called from a render method', () => { + class Parent extends React.Component { + render() { + return ; + } + } + + function Child() { + error('error: logged in child render method'); + warn('warn: logged in child render method'); + return null; + } + + ReactTestRenderer.create(); + + expectMessageAndStack( + 'logged in child render method', + '\n in Child (at **)' + '\n in Parent (at **)', + ); + }); + + it('includes component stack when called from a render phase lifecycle method', () => { + function Parent() { + return ; + } + + class Child extends React.Component { + UNSAFE_componentWillMount() { + error('error: logged in child cWM lifecycle'); + warn('warn: logged in child cWM lifecycle'); + } + render() { + return null; + } + } + + ReactTestRenderer.create(); + + expectMessageAndStack( + 'logged in child cWM lifecycle', + '\n in Child (at **)' + '\n in Parent (at **)', + ); + }); + + it('includes component stack when called from a commit phase lifecycle method', () => { + function Parent() { + return ; + } + + class Child extends React.Component { + componentDidMount() { + error('error: logged in child cDM lifecycle'); + warn('warn: logged in child cDM lifecycle'); + } + render() { + return null; + } + } + + ReactTestRenderer.create(); + + expectMessageAndStack( + 'logged in child cDM lifecycle', + '\n in Child (at **)' + '\n in Parent (at **)', + ); + }); + + it('includes component stack when called from a passive effect handler', () => { + class Parent extends React.Component { + render() { + return ; + } + } + + function Child() { + React.useEffect(() => { + error('error: logged in child render method'); + warn('warn: logged in child render method'); + }); + return null; + } + + ReactTestRenderer.create(); + + scheduler.flushAll(); // Flush passive effects + + expectMessageAndStack( + 'logged in child render method', + '\n in Child (at **)' + '\n in Parent (at **)', + ); + }); + } +}); diff --git a/packages/react/src/withComponentStack.js b/packages/react/src/withComponentStack.js new file mode 100644 index 0000000000000..62aa7ccde2009 --- /dev/null +++ b/packages/react/src/withComponentStack.js @@ -0,0 +1,48 @@ +/** + * 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. + */ + +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +function noop() {} + +let error = noop; +let warn = noop; +if (__DEV__) { + const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + + error = function() { + const stack = ReactDebugCurrentFrame.getStackAddendum(); + if (stack !== '') { + const length = arguments.length; + const args = new Array(length + 1); + for (let i = 0; i < length; i++) { + args[i] = arguments[i]; + } + args[length] = stack; + console.error.apply(console, args); + } else { + console.error.apply(console, arguments); + } + }; + + warn = function() { + const stack = ReactDebugCurrentFrame.getStackAddendum(); + if (stack !== '') { + const length = arguments.length; + const args = new Array(length + 1); + for (let i = 0; i < length; i++) { + args[i] = arguments[i]; + } + args[length] = stack; + console.warn.apply(console, args); + } else { + console.warn.apply(console, arguments); + } + }; +} + +export {error, warn};