React.warn() and React.error() (#15170)
Brian Vaughn committed
Mar 21, 2019 at 14:44 UTC
f161ee2eb7e78d6cb3d3878fe1812ac1057fedc6
3 files changed
+244
packages/react/src/React.js
+4
@@ -45,6 +45,7 @@ import {
45
cloneElementWithValidation,
46
} from './ReactElementValidator';
47
import ReactSharedInternals from './ReactSharedInternals';
48
+import {error, warn} from './withComponentStack';
49
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
50
51
const React = {
@@ -65,6 +66,9 @@ const React = {
66
lazy,
67
memo,
68
69
+ error,
70
+ warn,
71
+
72
useCallback,
73
useContext,
74
useEffect,
packages/react/src/__tests__/withComponentStack-test.js
new
+192
@@ -0,0 +1,192 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+function normalizeCodeLocInfo(str) {
13
+ return str && str.replace(/at .+?:\d+/g, 'at **');
14
+}
15
+
16
+function expectHelper(spy, prefix, ...expectedArgs) {
17
+ const expectedStack = expectedArgs.pop();
18
+
19
+ expect(spy).toHaveBeenCalledTimes(1);
20
+
21
+ const actualArgs = spy.calls.mostRecent().args;
22
+
23
+ let actualStack = undefined;
24
+ if (expectedStack !== undefined) {
25
+ actualStack = actualArgs.pop();
26
+ expect(normalizeCodeLocInfo(actualStack)).toBe(expectedStack);
27
+ }
28
+
29
+ expect(actualArgs).toHaveLength(expectedArgs.length);
30
+ actualArgs.forEach((actualArg, index) => {
31
+ const expectedArg = expectedArgs[index];
32
+ expect(actualArg).toBe(
33
+ index === 0 ? `${prefix}: ${expectedArg}` : expectedArg,
34
+ );
35
+ });
36
+}
37
+
38
+function expectMessageAndStack(...expectedArgs) {
39
+ expectHelper(console.error, 'error', ...expectedArgs);
40
+ expectHelper(console.warn, 'warn', ...expectedArgs);
41
+}
42
+
43
+describe('withComponentStack', () => {
44
+ let React = null;
45
+ let ReactTestRenderer = null;
46
+ let error = null;
47
+ let scheduler = null;
48
+ let warn = null;
49
+
50
+ beforeEach(() => {
51
+ jest.resetModules();
52
+ jest.mock('scheduler', () => require('scheduler/unstable_mock'));
53
+
54
+ React = require('react');
55
+ ReactTestRenderer = require('react-test-renderer');
56
+ scheduler = require('scheduler');
57
+
58
+ error = React.error;
59
+ warn = React.warn;
60
+
61
+ spyOnDevAndProd(console, 'error');
62
+ spyOnDevAndProd(console, 'warn');
63
+ });
64
+
65
+ if (!__DEV__) {
66
+ it('does nothing in production mode', () => {
67
+ error('error');
68
+ warn('warning');
69
+
70
+ expect(console.error).toHaveBeenCalledTimes(0);
71
+ expect(console.warn).toHaveBeenCalledTimes(0);
72
+ });
73
+ }
74
+
75
+ if (__DEV__) {
76
+ it('does not include component stack when called outside of render', () => {
77
+ error('error: logged outside of render');
78
+ warn('warn: logged outside of render');
79
+ expectMessageAndStack('logged outside of render', undefined);
80
+ });
81
+
82
+ it('should support multiple args', () => {
83
+ function Component() {
84
+ error('error: number:', 123, 'boolean:', true);
85
+ warn('warn: number:', 123, 'boolean:', true);
86
+ return null;
87
+ }
88
+
89
+ ReactTestRenderer.create(<Component />);
90
+
91
+ expectMessageAndStack(
92
+ 'number:',
93
+ 123,
94
+ 'boolean:',
95
+ true,
96
+ '\n in Component (at **)',
97
+ );
98
+ });
99
+
100
+ it('includes component stack when called from a render method', () => {
101
+ class Parent extends React.Component {
102
+ render() {
103
+ return <Child />;
104
+ }
105
+ }
106
+
107
+ function Child() {
108
+ error('error: logged in child render method');
109
+ warn('warn: logged in child render method');
110
+ return null;
111
+ }
112
+
113
+ ReactTestRenderer.create(<Parent />);
114
+
115
+ expectMessageAndStack(
116
+ 'logged in child render method',
117
+ '\n in Child (at **)' + '\n in Parent (at **)',
118
+ );
119
+ });
120
+
121
+ it('includes component stack when called from a render phase lifecycle method', () => {
122
+ function Parent() {
123
+ return <Child />;
124
+ }
125
+
126
+ class Child extends React.Component {
127
+ UNSAFE_componentWillMount() {
128
+ error('error: logged in child cWM lifecycle');
129
+ warn('warn: logged in child cWM lifecycle');
130
+ }
131
+ render() {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ ReactTestRenderer.create(<Parent />);
137
+
138
+ expectMessageAndStack(
139
+ 'logged in child cWM lifecycle',
140
+ '\n in Child (at **)' + '\n in Parent (at **)',
141
+ );
142
+ });
143
+
144
+ it('includes component stack when called from a commit phase lifecycle method', () => {
145
+ function Parent() {
146
+ return <Child />;
147
+ }
148
+
149
+ class Child extends React.Component {
150
+ componentDidMount() {
151
+ error('error: logged in child cDM lifecycle');
152
+ warn('warn: logged in child cDM lifecycle');
153
+ }
154
+ render() {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ ReactTestRenderer.create(<Parent />);
160
+
161
+ expectMessageAndStack(
162
+ 'logged in child cDM lifecycle',
163
+ '\n in Child (at **)' + '\n in Parent (at **)',
164
+ );
165
+ });
166
+
167
+ it('includes component stack when called from a passive effect handler', () => {
168
+ class Parent extends React.Component {
169
+ render() {
170
+ return <Child />;
171
+ }
172
+ }
173
+
174
+ function Child() {
175
+ React.useEffect(() => {
176
+ error('error: logged in child render method');
177
+ warn('warn: logged in child render method');
178
+ });
179
+ return null;
180
+ }
181
+
182
+ ReactTestRenderer.create(<Parent />);
183
+
184
+ scheduler.flushAll(); // Flush passive effects
185
+
186
+ expectMessageAndStack(
187
+ 'logged in child render method',
188
+ '\n in Child (at **)' + '\n in Parent (at **)',
189
+ );
190
+ });
191
+ }
192
+});
packages/react/src/withComponentStack.js
new
+48
@@ -0,0 +1,48 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import ReactSharedInternals from 'shared/ReactSharedInternals';
9
+
10
+function noop() {}
11
+
12
+let error = noop;
13
+let warn = noop;
14
+if (__DEV__) {
15
+ const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
16
+
17
+ error = function() {
18
+ const stack = ReactDebugCurrentFrame.getStackAddendum();
19
+ if (stack !== '') {
20
+ const length = arguments.length;
21
+ const args = new Array(length + 1);
22
+ for (let i = 0; i < length; i++) {
23
+ args[i] = arguments[i];
24
+ }
25
+ args[length] = stack;
26
+ console.error.apply(console, args);
27
+ } else {
28
+ console.error.apply(console, arguments);
29
+ }
30
+ };
31
+
32
+ warn = function() {
33
+ const stack = ReactDebugCurrentFrame.getStackAddendum();
34
+ if (stack !== '') {
35
+ const length = arguments.length;
36
+ const args = new Array(length + 1);
37
+ for (let i = 0; i < length; i++) {
38
+ args[i] = arguments[i];
39
+ }
40
+ args[length] = stack;
41
+ console.warn.apply(console, args);
42
+ } else {
43
+ console.warn.apply(console, arguments);
44
+ }
45
+ };
46
+}
47
+
48
+export {error, warn};