@samitouri / QOS-React / commits / 3cac0875dc

refactor[react-devtools]: move console patching to global hook (#30596)

Stacked on https://github.com/facebook/react/pull/30566 and whats under it. See [this commit](https://github.com/facebook/react/pull/30596/commits/374fd737e4b0b7028afb765838db7c0e22def865). It is mostly copying code from one place to another and updating tests. With these changes, for every console method that we patch, there is going to be a single applied patch: - For `error`, `warn`, and `trace` we are patching when hook is installed. This guarantees that component stacks are going to be appended even if browser DevTools are not opened. We pay some price for it, though: if user has browser DevTools closed and if at this point some warning or error is emitted (logged), the next time user opens browser DevTools, they are going to see `hook.js` as the source frame. Unfortunately, ignore listing from source maps is not applied retroactively, and I don't know if its a bug or just a design limitations. Once browser DevTools are opened, source maps will be loaded and ignore listing will be applied for all emitted logs in the future. - For `log`, `info`, `group`, `groupCollapsed` we are only patching when React notifies React DevTools about running in StrictMode. We unpatch the methods right after it.

Ruslan Lesiutin committed Sep 18, 2024 at 18:12 UTC 3cac0875dcd60b8db099d8fa671c5ad1f8f0ef23
13 files changed +680 -1383
packages/react-devtools-core/src/backend.js
-4
@@ -11,7 +11,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
11 import Bridge from 'react-devtools-shared/src/bridge';
12 import {installHook} from 'react-devtools-shared/src/hook';
13 import {initBackend} from 'react-devtools-shared/src/backend';
14 -import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
14 import {__DEBUG__} from 'react-devtools-shared/src/constants';
15 import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
16 import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
@@ -41,9 +40,6 @@ type ConnectOptions = {
40 devToolsSettingsManager: ?DevToolsSettingsManager,
41 };
42
44 -// Install a global variable to allow patching console early (during injection).
45 -// This provides React Native developers with components stacks even if they don't run DevTools.
46 -installConsoleFunctionsToWindow();
43 installHook(window);
44
45 const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
packages/react-devtools-inline/src/backend.js
-4
@@ -3,7 +3,6 @@
3 import Agent from 'react-devtools-shared/src/backend/agent';
4 import Bridge from 'react-devtools-shared/src/bridge';
5 import {initBackend} from 'react-devtools-shared/src/backend';
6 -import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
6 import {installHook} from 'react-devtools-shared/src/hook';
7 import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
8
@@ -120,8 +119,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
119 }
120
121 export function initialize(contentWindow: any): void {
123 - // Install a global variable to allow patching console early (during injection).
124 - // This provides React Native developers with components stacks even if they don't run DevTools.
125 - installConsoleFunctionsToWindow();
122 installHook(contentWindow);
123 }
packages/react-devtools-shared/src/__tests__/componentStacks-test.js
+31 -31
@@ -7,27 +7,17 @@
7 * @flow
8 */
9
10 -import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils';
10 +import {
11 + getVersionedRenderImplementation,
12 + normalizeCodeLocInfo,
13 +} from 'react-devtools-shared/src/__tests__/utils';
14
15 describe('component stack', () => {
16 let React;
17 let act;
15 - let mockError;
16 - let mockWarn;
18 let supportsOwnerStacks;
19
20 beforeEach(() => {
20 - // Intercept native console methods before DevTools bootstraps.
21 - // Normalize component stack locations.
22 - mockError = jest.fn();
23 - mockWarn = jest.fn();
24 - console.error = (...args) => {
25 - mockError(...args.map(normalizeCodeLocInfo));
26 - };
27 - console.warn = (...args) => {
28 - mockWarn(...args.map(normalizeCodeLocInfo));
29 - };
30 -
21 const utils = require('./utils');
22 act = utils.act;
23
@@ -54,18 +44,22 @@ describe('component stack', () => {
44
45 act(() => render(<Grandparent />));
46
57 - expect(mockError).toHaveBeenCalledWith(
47 + expect(
48 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
49 + ).toEqual([
50 'Test error.',
51 '\n in Child (at **)' +
52 '\n in Parent (at **)' +
53 '\n in Grandparent (at **)',
62 - );
63 - expect(mockWarn).toHaveBeenCalledWith(
54 + ]);
55 + expect(
56 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
57 + ).toEqual([
58 'Test warning.',
59 '\n in Child (at **)' +
60 '\n in Parent (at **)' +
61 '\n in Grandparent (at **)',
68 - );
62 + ]);
63 });
64
65 // This test should have caught #19911
@@ -89,13 +83,15 @@ describe('component stack', () => {
83
84 expect(useEffectCount).toBe(1);
85
92 - expect(mockWarn).toHaveBeenCalledWith(
86 + expect(
87 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
88 + ).toEqual([
89 'Warning to trigger appended component stacks.',
90 '\n in Example (at **)',
95 - );
91 + ]);
92 });
93
98 - // @reactVersion >=18.3
94 + // @reactVersion >= 18.3
95 it('should log the current component stack with debug info from promises', () => {
96 const Child = () => {
97 console.error('Test error.');
@@ -117,23 +113,27 @@ describe('component stack', () => {
113
114 act(() => render(<Grandparent />));
115
120 - expect(mockError).toHaveBeenCalledWith(
116 + expect(
117 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
118 + ).toEqual([
119 'Test error.',
120 supportsOwnerStacks
121 ? '\n in Child (at **)'
122 : '\n in Child (at **)' +
125 - '\n in ServerComponent (at **)' +
126 - '\n in Parent (at **)' +
127 - '\n in Grandparent (at **)',
128 - );
129 - expect(mockWarn).toHaveBeenCalledWith(
123 + '\n in ServerComponent (at **)' +
124 + '\n in Parent (at **)' +
125 + '\n in Grandparent (at **)',
126 + ]);
127 + expect(
128 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
129 + ).toEqual([
130 'Test warning.',
131 supportsOwnerStacks
132 ? '\n in Child (at **)'
133 : '\n in Child (at **)' +
134 - '\n in ServerComponent (at **)' +
135 - '\n in Parent (at **)' +
136 - '\n in Grandparent (at **)',
137 - );
134 + '\n in ServerComponent (at **)' +
135 + '\n in Parent (at **)' +
136 + '\n in Grandparent (at **)',
137 + ]);
138 });
139 });
packages/react-devtools-shared/src/__tests__/console-test.js
+275 -698
@@ -7,52 +7,25 @@
7 * @flow
8 */
9
10 -import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils';
10 +import {
11 + getVersionedRenderImplementation,
12 + normalizeCodeLocInfo,
13 +} from 'react-devtools-shared/src/__tests__/utils';
14
15 let React;
16 let ReactDOMClient;
17 let act;
15 -let fakeConsole;
16 -let mockError;
17 -let mockInfo;
18 -let mockGroup;
19 -let mockGroupCollapsed;
20 -let mockLog;
21 -let mockWarn;
22 -let patchConsole;
23 -let unpatchConsole;
18 let rendererID;
19 let supportsOwnerStacks = false;
20
21 describe('console', () => {
22 beforeEach(() => {
29 - const Console = require('react-devtools-shared/src/backend/console');
30 -
31 - patchConsole = Console.patch;
32 - unpatchConsole = Console.unpatch;
33 -
34 - // Patch a fake console so we can verify with tests below.
35 - // Patching the real console is too complicated,
36 - // because Jest itself has hooks into it as does our test env setup.
37 - mockError = jest.fn();
38 - mockInfo = jest.fn();
39 - mockGroup = jest.fn();
40 - mockGroupCollapsed = jest.fn();
41 - mockLog = jest.fn();
42 - mockWarn = jest.fn();
43 - fakeConsole = {
44 - error: mockError,
45 - info: mockInfo,
46 - log: mockLog,
47 - warn: mockWarn,
48 - group: mockGroup,
49 - groupCollapsed: mockGroupCollapsed,
50 - };
23 + const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
24 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
25 + rendererID = inject(internals);
26
52 - Console.dangerous_setTargetConsoleForTesting(fakeConsole);
53 - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.dangerous_setTargetConsoleForTesting(
54 - fakeConsole,
55 - );
27 + return rendererID;
28 + };
29
30 React = require('react');
31 if (
@@ -69,137 +42,44 @@ describe('console', () => {
42
43 const {render} = getVersionedRenderImplementation();
44
72 - // @reactVersion >=18.0
73 - it('should not patch console methods that are not explicitly overridden', () => {
74 - expect(fakeConsole.error).not.toBe(mockError);
75 - expect(fakeConsole.info).toBe(mockInfo);
76 - expect(fakeConsole.log).toBe(mockLog);
77 - expect(fakeConsole.warn).not.toBe(mockWarn);
78 - expect(fakeConsole.group).toBe(mockGroup);
79 - expect(fakeConsole.groupCollapsed).toBe(mockGroupCollapsed);
80 - });
81 -
82 - // @reactVersion >=18.0
83 - it('should patch the console when appendComponentStack is enabled', () => {
84 - unpatchConsole();
85 -
86 - expect(fakeConsole.error).toBe(mockError);
87 - expect(fakeConsole.warn).toBe(mockWarn);
88 -
89 - patchConsole({
90 - appendComponentStack: true,
91 - breakOnConsoleErrors: false,
92 - showInlineWarningsAndErrors: false,
93 - });
94 -
95 - expect(fakeConsole.error).not.toBe(mockError);
96 - expect(fakeConsole.warn).not.toBe(mockWarn);
97 - });
98 -
99 - // @reactVersion >=18.0
100 - it('should patch the console when breakOnConsoleErrors is enabled', () => {
101 - unpatchConsole();
102 -
103 - expect(fakeConsole.error).toBe(mockError);
104 - expect(fakeConsole.warn).toBe(mockWarn);
105 -
106 - patchConsole({
107 - appendComponentStack: false,
108 - breakOnConsoleErrors: true,
109 - showInlineWarningsAndErrors: false,
110 - });
111 -
112 - expect(fakeConsole.error).not.toBe(mockError);
113 - expect(fakeConsole.warn).not.toBe(mockWarn);
114 - });
115 -
116 - // @reactVersion >=18.0
117 - it('should patch the console when showInlineWarningsAndErrors is enabled', () => {
118 - unpatchConsole();
119 -
120 - expect(fakeConsole.error).toBe(mockError);
121 - expect(fakeConsole.warn).toBe(mockWarn);
122 -
123 - patchConsole({
124 - appendComponentStack: false,
125 - breakOnConsoleErrors: false,
126 - showInlineWarningsAndErrors: true,
127 - });
128 -
129 - expect(fakeConsole.error).not.toBe(mockError);
130 - expect(fakeConsole.warn).not.toBe(mockWarn);
131 - });
132 -
133 - // @reactVersion >=18.0
134 - it('should only patch the console once', () => {
135 - const {error, warn} = fakeConsole;
136 -
137 - patchConsole({
138 - appendComponentStack: true,
139 - breakOnConsoleErrors: false,
140 - showInlineWarningsAndErrors: false,
141 - });
142 -
143 - expect(fakeConsole.error).toBe(error);
144 - expect(fakeConsole.warn).toBe(warn);
145 - });
146 -
147 - // @reactVersion >=18.0
148 - it('should un-patch when requested', () => {
149 - expect(fakeConsole.error).not.toBe(mockError);
150 - expect(fakeConsole.warn).not.toBe(mockWarn);
45 + // @reactVersion >= 18.0
46 + it('should pass through logs when there is no current fiber', () => {
47 + expect(global.consoleLogMock).toHaveBeenCalledTimes(0);
48 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(0);
49 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(0);
50
152 - unpatchConsole();
51 + console.log('log');
52 + console.warn('warn');
53 + console.error('error');
54
154 - expect(fakeConsole.error).toBe(mockError);
155 - expect(fakeConsole.warn).toBe(mockWarn);
55 + expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
56 + expect(global.consoleWarnMock.mock.calls).toEqual([['warn']]);
57 + expect(global.consoleErrorMock.mock.calls).toEqual([['error']]);
58 });
59
158 - // @reactVersion >=18.0
159 - it('should pass through logs when there is no current fiber', () => {
160 - expect(mockLog).toHaveBeenCalledTimes(0);
161 - expect(mockWarn).toHaveBeenCalledTimes(0);
162 - expect(mockError).toHaveBeenCalledTimes(0);
163 - fakeConsole.log('log');
164 - fakeConsole.warn('warn');
165 - fakeConsole.error('error');
166 - expect(mockLog).toHaveBeenCalledTimes(1);
167 - expect(mockLog.mock.calls[0]).toHaveLength(1);
168 - expect(mockLog.mock.calls[0][0]).toBe('log');
169 - expect(mockWarn).toHaveBeenCalledTimes(1);
170 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
171 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
172 - expect(mockError).toHaveBeenCalledTimes(1);
173 - expect(mockError.mock.calls[0]).toHaveLength(1);
174 - expect(mockError.mock.calls[0][0]).toBe('error');
175 - });
176 -
177 - // @reactVersion >=18.0
60 + // @reactVersion >= 18.0
61 it('should not append multiple stacks', () => {
179 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
62 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
63
64 const Child = ({children}) => {
182 - fakeConsole.warn('warn\n in Child (at fake.js:123)');
183 - fakeConsole.error('error', '\n in Child (at fake.js:123)');
65 + console.warn('warn', '\n in Child (at fake.js:123)');
66 + console.error('error', '\n in Child (at fake.js:123)');
67 return null;
68 };
69
70 act(() => render(<Child />));
71
189 - expect(mockWarn).toHaveBeenCalledTimes(1);
190 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
191 - expect(mockWarn.mock.calls[0][0]).toBe(
192 - 'warn\n in Child (at fake.js:123)',
193 - );
194 - expect(mockError).toHaveBeenCalledTimes(1);
195 - expect(mockError.mock.calls[0]).toHaveLength(2);
196 - expect(mockError.mock.calls[0][0]).toBe('error');
197 - expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)');
72 + expect(
73 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
74 + ).toEqual(['warn', '\n in Child (at **)']);
75 + expect(
76 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
77 + ).toEqual(['error', '\n in Child (at **)']);
78 });
79
200 - // @reactVersion >=18.0
80 + // @reactVersion >= 18.0
81 it('should append component stacks to errors and warnings logged during render', () => {
202 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
82 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
83
84 const Intermediate = ({children}) => children;
85 const Parent = ({children}) => (
@@ -208,36 +88,34 @@ describe('console', () => {
88 </Intermediate>
89 );
90 const Child = ({children}) => {
211 - fakeConsole.error('error');
212 - fakeConsole.log('log');
213 - fakeConsole.warn('warn');
91 + console.error('error');
92 + console.log('log');
93 + console.warn('warn');
94 return null;
95 };
96
97 act(() => render(<Parent />));
98
219 - expect(mockLog).toHaveBeenCalledTimes(1);
220 - expect(mockLog.mock.calls[0]).toHaveLength(1);
221 - expect(mockLog.mock.calls[0][0]).toBe('log');
222 - expect(mockWarn).toHaveBeenCalledTimes(1);
223 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
224 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
225 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
99 + expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
100 + expect(
101 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
102 + ).toEqual([
103 + 'warn',
104 supportsOwnerStacks
105 ? '\n in Child (at **)\n in Parent (at **)'
106 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
229 - );
230 - expect(mockError).toHaveBeenCalledTimes(1);
231 - expect(mockError.mock.calls[0]).toHaveLength(2);
232 - expect(mockError.mock.calls[0][0]).toBe('error');
233 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
107 + ]);
108 + expect(
109 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
110 + ).toEqual([
111 + 'error',
112 supportsOwnerStacks
113 ? '\n in Child (at **)\n in Parent (at **)'
114 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
237 - );
115 + ]);
116 });
117
240 - // @reactVersion >=18.0
118 + // @reactVersion >= 18.0
119 it('should append component stacks to errors and warnings logged from effects', () => {
120 const Intermediate = ({children}) => children;
121 const Parent = ({children}) => (
@@ -247,60 +125,63 @@ describe('console', () => {
125 );
126 const Child = ({children}) => {
127 React.useLayoutEffect(function Child_useLayoutEffect() {
250 - fakeConsole.error('active error');
251 - fakeConsole.log('active log');
252 - fakeConsole.warn('active warn');
128 + console.error('active error');
129 + console.log('active log');
130 + console.warn('active warn');
131 });
132 React.useEffect(function Child_useEffect() {
255 - fakeConsole.error('passive error');
256 - fakeConsole.log('passive log');
257 - fakeConsole.warn('passive warn');
133 + console.error('passive error');
134 + console.log('passive log');
135 + console.warn('passive warn');
136 });
137 return null;
138 };
139
140 act(() => render(<Parent />));
141
264 - expect(mockLog).toHaveBeenCalledTimes(2);
265 - expect(mockLog.mock.calls[0]).toHaveLength(1);
266 - expect(mockLog.mock.calls[0][0]).toBe('active log');
267 - expect(mockLog.mock.calls[1]).toHaveLength(1);
268 - expect(mockLog.mock.calls[1][0]).toBe('passive log');
269 - expect(mockWarn).toHaveBeenCalledTimes(2);
270 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
271 - expect(mockWarn.mock.calls[0][0]).toBe('active warn');
272 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
142 + expect(global.consoleLogMock.mock.calls).toEqual([
143 + ['active log'],
144 + ['passive log'],
145 + ]);
146 +
147 + expect(
148 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
149 + ).toEqual([
150 + 'active warn',
151 supportsOwnerStacks
152 ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
153 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
276 - );
277 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
278 - expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
279 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
154 + ]);
155 + expect(
156 + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
157 + ).toEqual([
158 + 'passive warn',
159 supportsOwnerStacks
160 ? '\n in Child_useEffect (at **)\n in Parent (at **)'
161 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
283 - );
284 - expect(mockError).toHaveBeenCalledTimes(2);
285 - expect(mockError.mock.calls[0]).toHaveLength(2);
286 - expect(mockError.mock.calls[0][0]).toBe('active error');
287 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
162 + ]);
163 +
164 + expect(
165 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
166 + ).toEqual([
167 + 'active error',
168 supportsOwnerStacks
169 ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
170 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
291 - );
292 - expect(mockError.mock.calls[1]).toHaveLength(2);
293 - expect(mockError.mock.calls[1][0]).toBe('passive error');
294 - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
171 + ]);
172 + expect(
173 + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
174 + ).toEqual([
175 + 'passive error',
176 supportsOwnerStacks
177 ? '\n in Child_useEffect (at **)\n in Parent (at **)'
178 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
298 - );
179 + ]);
180 });
181
301 - // @reactVersion >=18.0
182 + // @reactVersion >= 18.0
183 it('should append component stacks to errors and warnings logged from commit hooks', () => {
303 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
184 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
185
186 const Intermediate = ({children}) => children;
187 const Parent = ({children}) => (
@@ -310,14 +191,14 @@ describe('console', () => {
191 );
192 class Child extends React.Component<any> {
193 componentDidMount() {
313 - fakeConsole.error('didMount error');
314 - fakeConsole.log('didMount log');
315 - fakeConsole.warn('didMount warn');
194 + console.error('didMount error');
195 + console.log('didMount log');
196 + console.warn('didMount warn');
197 }
198 componentDidUpdate() {
318 - fakeConsole.error('didUpdate error');
319 - fakeConsole.log('didUpdate log');
320 - fakeConsole.warn('didUpdate warn');
199 + console.error('didUpdate error');
200 + console.log('didUpdate log');
201 + console.warn('didUpdate warn');
202 }
203 render() {
204 return null;
@@ -327,44 +208,47 @@ describe('console', () => {
208 act(() => render(<Parent />));
209 act(() => render(<Parent />));
210
330 - expect(mockLog).toHaveBeenCalledTimes(2);
331 - expect(mockLog.mock.calls[0]).toHaveLength(1);
332 - expect(mockLog.mock.calls[0][0]).toBe('didMount log');
333 - expect(mockLog.mock.calls[1]).toHaveLength(1);
334 - expect(mockLog.mock.calls[1][0]).toBe('didUpdate log');
335 - expect(mockWarn).toHaveBeenCalledTimes(2);
336 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
337 - expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
338 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
211 + expect(global.consoleLogMock.mock.calls).toEqual([
212 + ['didMount log'],
213 + ['didUpdate log'],
214 + ]);
215 +
216 + expect(
217 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
218 + ).toEqual([
219 + 'didMount warn',
220 supportsOwnerStacks
221 ? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
222 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
342 - );
343 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
344 - expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
345 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
223 + ]);
224 + expect(
225 + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
226 + ).toEqual([
227 + 'didUpdate warn',
228 supportsOwnerStacks
229 ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
230 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
349 - );
350 - expect(mockError).toHaveBeenCalledTimes(2);
351 - expect(mockError.mock.calls[0]).toHaveLength(2);
352 - expect(mockError.mock.calls[0][0]).toBe('didMount error');
353 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
231 + ]);
232 +
233 + expect(
234 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
235 + ).toEqual([
236 + 'didMount error',
237 supportsOwnerStacks
238 ? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
239 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
357 - );
358 - expect(mockError.mock.calls[1]).toHaveLength(2);
359 - expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
360 - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
240 + ]);
241 + expect(
242 + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
243 + ).toEqual([
244 + 'didUpdate error',
245 supportsOwnerStacks
246 ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
247 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
364 - );
248 + ]);
249 });
250
367 - // @reactVersion >=18.0
251 + // @reactVersion >= 18.0
252 it('should append component stacks to errors and warnings logged from gDSFP', () => {
253 const Intermediate = ({children}) => children;
254 const Parent = ({children}) => (
@@ -375,9 +259,9 @@ describe('console', () => {
259 class Child extends React.Component<any, any> {
260 state = {};
261 static getDerivedStateFromProps() {
378 - fakeConsole.error('error');
379 - fakeConsole.log('log');
380 - fakeConsole.warn('warn');
262 + console.error('error');
263 + console.log('log');
264 + console.warn('warn');
265 return null;
266 }
267 render() {
@@ -387,71 +271,27 @@ describe('console', () => {
271
272 act(() => render(<Parent />));
273
390 - expect(mockLog).toHaveBeenCalledTimes(1);
391 - expect(mockLog.mock.calls[0]).toHaveLength(1);
392 - expect(mockLog.mock.calls[0][0]).toBe('log');
393 - expect(mockWarn).toHaveBeenCalledTimes(1);
394 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
395 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
396 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
274 + expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
275 + expect(
276 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
277 + ).toEqual([
278 + 'warn',
279 supportsOwnerStacks
280 ? '\n in Parent (at **)'
281 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
400 - );
401 - expect(mockError).toHaveBeenCalledTimes(1);
402 - expect(mockError.mock.calls[0]).toHaveLength(2);
403 - expect(mockError.mock.calls[0][0]).toBe('error');
404 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
282 + ]);
283 + expect(
284 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
285 + ).toEqual([
286 + 'error',
287 supportsOwnerStacks
288 ? '\n in Parent (at **)'
289 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
408 - );
409 - });
410 -
411 - // @reactVersion >=18.0
412 - it('should append stacks after being uninstalled and reinstalled', () => {
413 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
414 -
415 - const Child = ({children}) => {
416 - fakeConsole.warn('warn');
417 - fakeConsole.error('error');
418 - return null;
419 - };
420 -
421 - act(() => render(<Child />));
422 -
423 - expect(mockWarn).toHaveBeenCalledTimes(1);
424 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
425 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
426 - expect(mockError).toHaveBeenCalledTimes(1);
427 - expect(mockError.mock.calls[0]).toHaveLength(1);
428 - expect(mockError.mock.calls[0][0]).toBe('error');
429 -
430 - patchConsole({
431 - appendComponentStack: true,
432 - breakOnConsoleErrors: false,
433 - showInlineWarningsAndErrors: false,
434 - });
435 - act(() => render(<Child />));
436 -
437 - expect(mockWarn).toHaveBeenCalledTimes(2);
438 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
439 - expect(mockWarn.mock.calls[1][0]).toBe('warn');
440 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
441 - '\n in Child (at **)',
442 - );
443 - expect(mockError).toHaveBeenCalledTimes(2);
444 - expect(mockError.mock.calls[1]).toHaveLength(2);
445 - expect(mockError.mock.calls[1][0]).toBe('error');
446 - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
447 - '\n in Child (at **)',
448 - );
290 + ]);
291 });
292
451 - // @reactVersion >=18.0
293 + // @reactVersion >= 18.0
294 it('should be resilient to prepareStackTrace', () => {
453 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
454 -
295 Error.prepareStackTrace = function (error, callsites) {
296 const stack = ['An error occurred:', error.message];
297 for (let i = 0; i < callsites.length; i++) {
@@ -473,62 +313,66 @@ describe('console', () => {
313 </Intermediate>
314 );
315 const Child = ({children}) => {
476 - fakeConsole.error('error');
477 - fakeConsole.log('log');
478 - fakeConsole.warn('warn');
316 + console.error('error');
317 + console.log('log');
318 + console.warn('warn');
319 return null;
320 };
321
322 act(() => render(<Parent />));
323
484 - expect(mockLog).toHaveBeenCalledTimes(1);
485 - expect(mockLog.mock.calls[0]).toHaveLength(1);
486 - expect(mockLog.mock.calls[0][0]).toBe('log');
487 - expect(mockWarn).toHaveBeenCalledTimes(1);
488 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
489 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
490 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
324 + expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
325 + expect(
326 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
327 + ).toEqual([
328 + 'warn',
329 supportsOwnerStacks
330 ? '\n in Child (at **)\n in Parent (at **)'
331 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
494 - );
495 - expect(mockError).toHaveBeenCalledTimes(1);
496 - expect(mockError.mock.calls[0]).toHaveLength(2);
497 - expect(mockError.mock.calls[0][0]).toBe('error');
498 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
332 + ]);
333 + expect(
334 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
335 + ).toEqual([
336 + 'error',
337 supportsOwnerStacks
338 ? '\n in Child (at **)\n in Parent (at **)'
339 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
502 - );
340 + ]);
341 });
342
505 - // @reactVersion >=18.0
343 + // @reactVersion >= 18.0
344 it('should correctly log Symbols', () => {
345 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
346 +
347 const Component = ({children}) => {
508 - fakeConsole.warn('Symbol:', Symbol(''));
348 + console.warn('Symbol:', Symbol(''));
349 return null;
350 };
351
352 act(() => render(<Component />));
353
514 - expect(mockWarn).toHaveBeenCalledTimes(1);
515 - expect(mockWarn.mock.calls[0][0]).toBe('Symbol:');
354 + expect(global.consoleWarnMock.mock.calls).toMatchInlineSnapshot(`
355 + [
356 + [
357 + "Symbol:",
358 + Symbol(),
359 + ],
360 + ]
361 + `);
362 });
363
364 it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
519 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
520 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
365 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
366 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
367 + false;
368
369 const container = document.createElement('div');
370 const root = ReactDOMClient.createRoot(container);
371
372 function App() {
526 - fakeConsole.log('log');
527 - fakeConsole.warn('warn');
528 - fakeConsole.error('error');
529 - fakeConsole.info('info');
530 - fakeConsole.group('group');
531 - fakeConsole.groupCollapsed('groupCollapsed');
373 + console.log('log');
374 + console.warn('warn');
375 + console.error('error');
376 return <div />;
377 }
378
@@ -539,77 +383,38 @@ describe('console', () => {
383 </React.StrictMode>,
384 ),
385 );
542 - expect(mockLog.mock.calls[0]).toHaveLength(1);
543 - expect(mockLog.mock.calls[0][0]).toBe('log');
544 - expect(mockLog.mock.calls[1]).toEqual([
386 +
387 + expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
388 + expect(global.consoleLogMock.mock.calls[1]).toEqual([
389 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
390 'log',
391 ]);
392
549 - expect(mockWarn).toHaveBeenCalledTimes(2);
550 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
551 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
552 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
553 - expect(mockWarn.mock.calls[1]).toEqual([
393 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
394 + expect(global.consoleWarnMock.mock.calls[1]).toEqual([
395 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
396 'warn',
397 ]);
398
558 - expect(mockError).toHaveBeenCalledTimes(2);
559 - expect(mockError.mock.calls[0]).toHaveLength(1);
560 - expect(mockError.mock.calls[0][0]).toBe('error');
561 - expect(mockError.mock.calls[1]).toHaveLength(2);
562 - expect(mockError.mock.calls[1]).toEqual([
399 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
400 + expect(global.consoleErrorMock.mock.calls[1]).toEqual([
401 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
402 'error',
403 ]);
566 -
567 - expect(mockInfo).toHaveBeenCalledTimes(2);
568 - expect(mockInfo.mock.calls[0]).toHaveLength(1);
569 - expect(mockInfo.mock.calls[0][0]).toBe('info');
570 - expect(mockInfo.mock.calls[1]).toHaveLength(2);
571 - expect(mockInfo.mock.calls[1]).toEqual([
572 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
573 - 'info',
574 - ]);
575 -
576 - expect(mockGroup).toHaveBeenCalledTimes(2);
577 - expect(mockGroup.mock.calls[0]).toHaveLength(1);
578 - expect(mockGroup.mock.calls[0][0]).toBe('group');
579 - expect(mockGroup.mock.calls[1]).toHaveLength(2);
580 - expect(mockGroup.mock.calls[1]).toEqual([
581 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
582 - 'group',
583 - ]);
584 -
585 - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2);
586 - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
587 - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
588 - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2);
589 - expect(mockGroupCollapsed.mock.calls[1]).toEqual([
590 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
591 - 'groupCollapsed',
592 - ]);
404 });
405
406 it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
596 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
597 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
407 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
408 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
409 + true;
410
411 const container = document.createElement('div');
412 const root = ReactDOMClient.createRoot(container);
413
414 function App() {
603 - console.log(
604 - 'CALL',
605 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__,
606 - );
607 - fakeConsole.log('log');
608 - fakeConsole.warn('warn');
609 - fakeConsole.error('error');
610 - fakeConsole.info('info');
611 - fakeConsole.group('group');
612 - fakeConsole.groupCollapsed('groupCollapsed');
415 + console.log('log');
416 + console.warn('warn');
417 + console.error('error');
418 return <div />;
419 }
420
@@ -621,54 +426,29 @@ describe('console', () => {
426 ),
427 );
428
624 - expect(mockLog).toHaveBeenCalledTimes(1);
625 - expect(mockLog.mock.calls[0]).toHaveLength(1);
626 - expect(mockLog.mock.calls[0][0]).toBe('log');
627 -
628 - expect(mockWarn).toHaveBeenCalledTimes(1);
629 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
630 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
631 -
632 - expect(mockError).toHaveBeenCalledTimes(1);
633 - expect(mockError.mock.calls[0]).toHaveLength(1);
634 - expect(mockError.mock.calls[0][0]).toBe('error');
635 -
636 - expect(mockInfo).toHaveBeenCalledTimes(1);
637 - expect(mockInfo.mock.calls[0]).toHaveLength(1);
638 - expect(mockInfo.mock.calls[0][0]).toBe('info');
639 -
640 - expect(mockGroup).toHaveBeenCalledTimes(1);
641 - expect(mockGroup.mock.calls[0]).toHaveLength(1);
642 - expect(mockGroup.mock.calls[0][0]).toBe('group');
643 -
644 - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1);
645 - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
646 - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
429 + expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
430 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
431 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
432 });
433
434 it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
650 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
651 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
435 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
436 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
437 + false;
438
439 const container = document.createElement('div');
440 const root = ReactDOMClient.createRoot(container);
441
442 function App() {
443 React.useEffect(() => {
658 - fakeConsole.log('log effect create');
659 - fakeConsole.warn('warn effect create');
660 - fakeConsole.error('error effect create');
661 - fakeConsole.info('info effect create');
662 - fakeConsole.group('group effect create');
663 - fakeConsole.groupCollapsed('groupCollapsed effect create');
444 + console.log('log effect create');
445 + console.warn('warn effect create');
446 + console.error('error effect create');
447
448 return () => {
666 - fakeConsole.log('log effect cleanup');
667 - fakeConsole.warn('warn effect cleanup');
668 - fakeConsole.error('error effect cleanup');
669 - fakeConsole.info('info effect cleanup');
670 - fakeConsole.group('group effect cleanup');
671 - fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
449 + console.log('log effect cleanup');
450 + console.warn('warn effect cleanup');
451 + console.error('error effect cleanup');
452 };
453 });
454
@@ -682,61 +462,41 @@ describe('console', () => {
462 </React.StrictMode>,
463 ),
464 );
685 - expect(mockLog.mock.calls).toEqual([
465 + expect(global.consoleLogMock.mock.calls).toEqual([
466 ['log effect create'],
467 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect cleanup'],
468 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect create'],
469 ]);
690 - expect(mockWarn.mock.calls).toEqual([
470 + expect(global.consoleWarnMock.mock.calls).toEqual([
471 ['warn effect create'],
472 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect cleanup'],
473 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect create'],
474 ]);
695 - expect(mockError.mock.calls).toEqual([
475 + expect(global.consoleErrorMock.mock.calls).toEqual([
476 ['error effect create'],
477 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect cleanup'],
478 ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect create'],
479 ]);
700 - expect(mockInfo.mock.calls).toEqual([
701 - ['info effect create'],
702 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect cleanup'],
703 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect create'],
704 - ]);
705 - expect(mockGroup.mock.calls).toEqual([
706 - ['group effect create'],
707 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect cleanup'],
708 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect create'],
709 - ]);
710 - expect(mockGroupCollapsed.mock.calls).toEqual([
711 - ['groupCollapsed effect create'],
712 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect cleanup'],
713 - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect create'],
714 - ]);
480 });
481
482 it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
718 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
719 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
483 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
484 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
485 + true;
486
487 const container = document.createElement('div');
488 const root = ReactDOMClient.createRoot(container);
489
490 function App() {
491 React.useEffect(() => {
726 - fakeConsole.log('log effect create');
727 - fakeConsole.warn('warn effect create');
728 - fakeConsole.error('error effect create');
729 - fakeConsole.info('info effect create');
730 - fakeConsole.group('group effect create');
731 - fakeConsole.groupCollapsed('groupCollapsed effect create');
492 + console.log('log effect create');
493 + console.warn('warn effect create');
494 + console.error('error effect create');
495
496 return () => {
734 - fakeConsole.log('log effect cleanup');
735 - fakeConsole.warn('warn effect cleanup');
736 - fakeConsole.error('error effect cleanup');
737 - fakeConsole.info('info effect cleanup');
738 - fakeConsole.group('group effect cleanup');
739 - fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
497 + console.log('log effect cleanup');
498 + console.warn('warn effect cleanup');
499 + console.error('error effect cleanup');
500 };
501 });
502
@@ -750,31 +510,25 @@ describe('console', () => {
510 </React.StrictMode>,
511 ),
512 );
753 - expect(mockLog.mock.calls).toEqual([['log effect create']]);
754 - expect(mockWarn.mock.calls).toEqual([['warn effect create']]);
755 - expect(mockError.mock.calls).toEqual([['error effect create']]);
756 - expect(mockInfo.mock.calls).toEqual([['info effect create']]);
757 - expect(mockGroup.mock.calls).toEqual([['group effect create']]);
758 - expect(mockGroupCollapsed.mock.calls).toEqual([
759 - ['groupCollapsed effect create'],
760 - ]);
513 +
514 + expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
515 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
516 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
517 });
518
519 it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
764 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
765 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
520 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
521 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
522 + false;
523
524 const container = document.createElement('div');
525 const root = ReactDOMClient.createRoot(container);
526
527 function App() {
528 React.useMemo(() => {
772 - fakeConsole.log('log');
773 - fakeConsole.warn('warn');
774 - fakeConsole.error('error');
775 - fakeConsole.info('info');
776 - fakeConsole.group('group');
777 - fakeConsole.groupCollapsed('groupCollapsed');
529 + console.log('log');
530 + console.warn('warn');
531 + console.error('error');
532 }, []);
533 return <div />;
534 }
@@ -786,78 +540,39 @@ describe('console', () => {
540 </React.StrictMode>,
541 ),
542 );
789 - expect(mockLog.mock.calls[0]).toHaveLength(1);
790 - expect(mockLog.mock.calls[0][0]).toBe('log');
791 - expect(mockLog.mock.calls[1]).toEqual([
543 +
544 + expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
545 + expect(global.consoleLogMock.mock.calls[1]).toEqual([
546 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
547 'log',
548 ]);
549
796 - expect(mockWarn).toHaveBeenCalledTimes(2);
797 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
798 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
799 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
800 - expect(mockWarn.mock.calls[1]).toEqual([
550 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
551 + expect(global.consoleWarnMock.mock.calls[1]).toEqual([
552 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
553 'warn',
554 ]);
555
805 - expect(mockError).toHaveBeenCalledTimes(2);
806 - expect(mockError.mock.calls[0]).toHaveLength(1);
807 - expect(mockError.mock.calls[0][0]).toBe('error');
808 - expect(mockError.mock.calls[1]).toHaveLength(2);
809 - expect(mockError.mock.calls[1]).toEqual([
556 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
557 + expect(global.consoleErrorMock.mock.calls[1]).toEqual([
558 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
559 'error',
560 ]);
813 -
814 - expect(mockInfo).toHaveBeenCalledTimes(2);
815 - expect(mockInfo.mock.calls[0]).toHaveLength(1);
816 - expect(mockInfo.mock.calls[0][0]).toBe('info');
817 - expect(mockInfo.mock.calls[1]).toHaveLength(2);
818 - expect(mockInfo.mock.calls[1]).toEqual([
819 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
820 - 'info',
821 - ]);
822 -
823 - expect(mockGroup).toHaveBeenCalledTimes(2);
824 - expect(mockGroup.mock.calls[0]).toHaveLength(1);
825 - expect(mockGroup.mock.calls[0][0]).toBe('group');
826 - expect(mockGroup.mock.calls[1]).toHaveLength(2);
827 - expect(mockGroup.mock.calls[1]).toEqual([
828 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
829 - 'group',
830 - ]);
831 -
832 - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2);
833 - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
834 - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
835 - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2);
836 - expect(mockGroupCollapsed.mock.calls[1]).toEqual([
837 - '\x1b[2;38;2;124;124;124m%s\x1b[0m',
838 - 'groupCollapsed',
839 - ]);
561 });
562
563 it('should not double log from useMemo fns if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
843 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
844 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
564 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
565 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
566 + true;
567
568 const container = document.createElement('div');
569 const root = ReactDOMClient.createRoot(container);
570
571 function App() {
572 React.useMemo(() => {
851 - console.log(
852 - 'CALL',
853 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__,
854 - );
855 - fakeConsole.log('log');
856 - fakeConsole.warn('warn');
857 - fakeConsole.error('error');
858 - fakeConsole.info('info');
859 - fakeConsole.group('group');
860 - fakeConsole.groupCollapsed('groupCollapsed');
573 + console.log('log');
574 + console.warn('warn');
575 + console.error('error');
576 }, []);
577 return <div />;
578 }
@@ -870,49 +585,27 @@ describe('console', () => {
585 ),
586 );
587
873 - expect(mockLog).toHaveBeenCalledTimes(1);
874 - expect(mockLog.mock.calls[0]).toHaveLength(1);
875 - expect(mockLog.mock.calls[0][0]).toBe('log');
876 -
877 - expect(mockWarn).toHaveBeenCalledTimes(1);
878 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
879 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
880 -
881 - expect(mockError).toHaveBeenCalledTimes(1);
882 - expect(mockError.mock.calls[0]).toHaveLength(1);
883 - expect(mockError.mock.calls[0][0]).toBe('error');
884 -
885 - expect(mockInfo).toHaveBeenCalledTimes(1);
886 - expect(mockInfo.mock.calls[0]).toHaveLength(1);
887 - expect(mockInfo.mock.calls[0][0]).toBe('info');
888 -
889 - expect(mockGroup).toHaveBeenCalledTimes(1);
890 - expect(mockGroup.mock.calls[0]).toHaveLength(1);
891 - expect(mockGroup.mock.calls[0][0]).toBe('group');
892 -
893 - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1);
894 - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
895 - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
588 + expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
589 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
590 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
591 });
592
593 it('should double log in Strict mode initial render for extension', () => {
899 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
900 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
594 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
595 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
596 + false;
597
598 // This simulates a render that happens before React DevTools have finished
599 // their handshake to attach the React DOM renderer functions to DevTools
600 // In this case, we should still be able to mock the console in Strict mode
905 - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
906 - rendererID,
907 - null,
908 - );
601 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID);
602 const container = document.createElement('div');
603 const root = ReactDOMClient.createRoot(container);
604
605 function App() {
913 - fakeConsole.log('log');
914 - fakeConsole.warn('warn');
915 - fakeConsole.error('error');
606 + console.log('log');
607 + console.warn('warn');
608 + console.error('error');
609 return <div />;
610 }
611
@@ -924,52 +617,41 @@ describe('console', () => {
617 ),
618 );
619
927 - expect(mockLog).toHaveBeenCalledTimes(2);
928 - expect(mockLog.mock.calls[0]).toHaveLength(1);
929 - expect(mockLog.mock.calls[0][0]).toBe('log');
930 - expect(mockLog.mock.calls[1]).toHaveLength(2);
931 - expect(mockLog.mock.calls[1]).toEqual([
620 + expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
621 + expect(global.consoleLogMock.mock.calls[1]).toEqual([
622 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
623 'log',
624 ]);
625
936 - expect(mockWarn).toHaveBeenCalledTimes(2);
937 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
938 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
939 - expect(mockWarn.mock.calls[1]).toHaveLength(2);
940 - expect(mockWarn.mock.calls[1]).toEqual([
626 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
627 + expect(global.consoleWarnMock.mock.calls[1]).toEqual([
628 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
629 'warn',
630 ]);
631
945 - expect(mockError).toHaveBeenCalledTimes(2);
946 - expect(mockError.mock.calls[0]).toHaveLength(1);
947 - expect(mockError.mock.calls[0][0]).toBe('error');
948 - expect(mockError.mock.calls[1]).toHaveLength(2);
949 - expect(mockError.mock.calls[1]).toEqual([
632 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
633 + expect(global.consoleErrorMock.mock.calls[1]).toEqual([
634 '\x1b[2;38;2;124;124;124m%s\x1b[0m',
635 'error',
636 ]);
637 });
638
639 it('should not double log in Strict mode initial render for extension', () => {
956 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
957 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
640 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
641 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
642 + true;
643
644 // This simulates a render that happens before React DevTools have finished
645 // their handshake to attach the React DOM renderer functions to DevTools
646 // In this case, we should still be able to mock the console in Strict mode
962 - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
963 - rendererID,
964 - null,
965 - );
647 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID);
648 const container = document.createElement('div');
649 const root = ReactDOMClient.createRoot(container);
650
651 function App() {
970 - fakeConsole.log('log');
971 - fakeConsole.warn('warn');
972 - fakeConsole.error('error');
652 + console.log('log');
653 + console.warn('warn');
654 + console.error('error');
655 return <div />;
656 }
657
@@ -980,22 +662,16 @@ describe('console', () => {
662 </React.StrictMode>,
663 ),
664 );
983 - expect(mockLog).toHaveBeenCalledTimes(1);
984 - expect(mockLog.mock.calls[0]).toHaveLength(1);
985 - expect(mockLog.mock.calls[0][0]).toBe('log');
665
987 - expect(mockWarn).toHaveBeenCalledTimes(1);
988 - expect(mockWarn.mock.calls[0]).toHaveLength(1);
989 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
990 -
991 - expect(mockError).toHaveBeenCalledTimes(1);
992 - expect(mockError.mock.calls[0]).toHaveLength(1);
993 - expect(mockError.mock.calls[0][0]).toBe('error');
666 + expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
667 + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
668 + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
669 });
670
671 it('should properly dim component stacks during strict mode double log', () => {
997 - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
998 - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
672 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
673 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
674 + false;
675
676 const container = document.createElement('div');
677 const root = ReactDOMClient.createRoot(container);
@@ -1007,8 +683,8 @@ describe('console', () => {
683 </Intermediate>
684 );
685 const Child = ({children}) => {
1010 - fakeConsole.error('error');
1011 - fakeConsole.warn('warn');
686 + console.error('error');
687 + console.warn('warn');
688 return null;
689 };
690
@@ -1020,140 +696,41 @@ describe('console', () => {
696 ),
697 );
698
1023 - expect(mockWarn).toHaveBeenCalledTimes(2);
1024 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
1025 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
699 + expect(
700 + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
701 + ).toEqual([
702 + 'warn',
703 supportsOwnerStacks
704 ? '\n in Child (at **)\n in Parent (at **)'
705 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1029 - );
1030 - expect(mockWarn.mock.calls[1]).toHaveLength(3);
1031 - expect(mockWarn.mock.calls[1][0]).toEqual(
706 + ]);
707 +
708 + expect(
709 + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
710 + ).toEqual([
711 '\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
1033 - );
1034 - expect(mockWarn.mock.calls[1][1]).toMatch('warn');
1035 - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual(
712 + 'warn',
713 supportsOwnerStacks
1037 - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist.
1038 - '\n in Child (at **)\n in Parent (at **)'
714 + ? '\n in Child (at **)\n in Parent (at **)'
715 : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1040 - );
716 + ]);
717
1042 - expect(mockError).toHaveBeenCalledTimes(2);
1043 - expect(mockError.mock.calls[0]).toHaveLength(2);
1044 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual(
718 + expect(
719 + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
720 + ).toEqual([
721 + 'error',
722 supportsOwnerStacks
723 ? '\n in Child (at **)\n in Parent (at **)'
724 : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1048 - );
1049 - expect(mockError.mock.calls[1]).toHaveLength(3);
1050 - expect(mockError.mock.calls[1][0]).toEqual(
725 + ]);
726 + expect(
727 + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
728 + ).toEqual([
729 '\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
1052 - );
1053 - expect(mockError.mock.calls[1][1]).toEqual('error');
1054 - expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual(
730 + 'error',
731 supportsOwnerStacks
1056 - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist.
1057 - '\n in Child (at **)\n in Parent (at **)'
732 + ? '\n in Child (at **)\n in Parent (at **)'
733 : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
1059 - );
1060 - });
1061 -});
1062 -
1063 -describe('console error', () => {
1064 - beforeEach(() => {
1065 - jest.resetModules();
1066 -
1067 - const Console = require('react-devtools-shared/src/backend/console');
1068 - patchConsole = Console.patch;
1069 - unpatchConsole = Console.unpatch;
1070 -
1071 - // Patch a fake console so we can verify with tests below.
1072 - // Patching the real console is too complicated,
1073 - // because Jest itself has hooks into it as does our test env setup.
1074 - mockError = jest.fn();
1075 - mockInfo = jest.fn();
1076 - mockGroup = jest.fn();
1077 - mockGroupCollapsed = jest.fn();
1078 - mockLog = jest.fn();
1079 - mockWarn = jest.fn();
1080 - fakeConsole = {
1081 - error: mockError,
1082 - info: mockInfo,
1083 - log: mockLog,
1084 - warn: mockWarn,
1085 - group: mockGroup,
1086 - groupCollapsed: mockGroupCollapsed,
1087 - };
1088 -
1089 - Console.dangerous_setTargetConsoleForTesting(fakeConsole);
1090 -
1091 - const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
1092 - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
1093 - inject(internals);
1094 -
1095 - Console.registerRenderer(
1096 - () => {
1097 - throw Error('foo');
1098 - },
1099 - () => {
1100 - return {
1101 - enableOwnerStacks: true,
1102 - componentStack: '\n at FakeStack (fake-file)',
1103 - };
1104 - },
1105 - );
1106 - };
1107 -
1108 - React = require('react');
1109 - ReactDOMClient = require('react-dom/client');
1110 -
1111 - const utils = require('./utils');
1112 - act = utils.act;
1113 - });
1114 -
1115 - // @reactVersion >=18.0
1116 - it('error in console log throws without interfering with logging', () => {
1117 - const container = document.createElement('div');
1118 - const root = ReactDOMClient.createRoot(container);
1119 -
1120 - function App() {
1121 - fakeConsole.log('log');
1122 - fakeConsole.warn('warn');
1123 - fakeConsole.error('error');
1124 - return <div />;
1125 - }
1126 -
1127 - patchConsole({
1128 - appendComponentStack: true,
1129 - breakOnConsoleErrors: false,
1130 - showInlineWarningsAndErrors: true,
1131 - hideConsoleLogsInStrictMode: false,
1132 - });
1133 -
1134 - expect(() => {
1135 - act(() => {
1136 - root.render(<App />);
1137 - });
1138 - }).toThrowError('foo');
1139 -
1140 - expect(mockLog).toHaveBeenCalledTimes(1);
1141 - expect(mockLog.mock.calls[0]).toHaveLength(1);
1142 - expect(mockLog.mock.calls[0][0]).toBe('log');
1143 -
1144 - expect(mockWarn).toHaveBeenCalledTimes(1);
1145 - expect(mockWarn.mock.calls[0]).toHaveLength(2);
1146 - expect(mockWarn.mock.calls[0][0]).toBe('warn');
1147 - // An error in showInlineWarningsAndErrors doesn't need to break component stacks.
1148 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1149 - '\n in FakeStack (at **)',
1150 - );
1151 -
1152 - expect(mockError).toHaveBeenCalledTimes(1);
1153 - expect(mockError.mock.calls[0]).toHaveLength(2);
1154 - expect(mockError.mock.calls[0][0]).toBe('error');
1155 - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1156 - '\n in FakeStack (at **)',
1157 - );
734 + ]);
735 });
736 });
packages/react-devtools-shared/src/__tests__/setupTests.js
+88 -56
@@ -13,6 +13,7 @@ import type {
13 BackendBridge,
14 FrontendBridge,
15 } from 'react-devtools-shared/src/bridge';
16 +
17 const {getTestFlags} = require('../../../../scripts/jest/TestFlags');
18
19 // Argument is serialized when passed from jest-cli script through to setupTests.
@@ -103,61 +104,36 @@ global.gate = fn => {
104 return fn(flags);
105 };
106
106 -beforeEach(() => {
107 - global.mockClipboardCopy = jest.fn();
108 -
109 - // Test environment doesn't support document methods like execCommand()
110 - // Also once the backend components below have been required,
111 - // it's too late for a test to mock the clipboard-js modules.
112 - jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
113 -
114 - // These files should be required (and re-required) before each test,
115 - // rather than imported at the head of the module.
116 - // That's because we reset modules between tests,
117 - // which disconnects the DevTool's cache from the current dispatcher ref.
118 - const Agent = require('react-devtools-shared/src/backend/agent').default;
119 - const {initBackend} = require('react-devtools-shared/src/backend');
120 - const Bridge = require('react-devtools-shared/src/bridge').default;
121 - const Store = require('react-devtools-shared/src/devtools/store').default;
122 - const {installHook} = require('react-devtools-shared/src/hook');
123 - const {
124 - getDefaultComponentFilters,
125 - setSavedComponentFilters,
126 - } = require('react-devtools-shared/src/utils');
107 +function shouldIgnoreConsoleErrorOrWarn(args) {
108 + let firstArg = args[0];
109 + if (
110 + firstArg !== null &&
111 + typeof firstArg === 'object' &&
112 + String(firstArg).indexOf('Error: Uncaught [') === 0
113 + ) {
114 + firstArg = String(firstArg);
115 + } else if (typeof firstArg !== 'string') {
116 + return false;
117 + }
118
128 - // Fake timers let us flush Bridge operations between setup and assertions.
129 - jest.useFakeTimers();
119 + return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
120 + return firstArg.indexOf(errorOrWarningMessage) !== -1;
121 + });
122 +}
123
131 - // We use fake timers heavily in tests but the bridge batching now uses microtasks.
132 - global.devtoolsJestTestScheduler = callback => {
133 - setTimeout(callback, 0);
134 - };
124 +function patchConsoleForTestingBeforeHookInstallation() {
125 + const originalConsoleError = console.error;
126 + const originalConsoleWarn = console.warn;
127 + const originalConsoleLog = console.log;
128
136 - // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
137 - global._ignoredErrorOrWarningMessages = [
138 - 'react-test-renderer is deprecated.',
139 - ];
140 - function shouldIgnoreConsoleErrorOrWarn(args) {
141 - let firstArg = args[0];
142 - if (
143 - firstArg !== null &&
144 - typeof firstArg === 'object' &&
145 - String(firstArg).indexOf('Error: Uncaught [') === 0
146 - ) {
147 - firstArg = String(firstArg);
148 - } else if (typeof firstArg !== 'string') {
149 - return false;
150 - }
151 - const shouldFilter = global._ignoredErrorOrWarningMessages.some(
152 - errorOrWarningMessage => {
153 - return firstArg.indexOf(errorOrWarningMessage) !== -1;
154 - },
155 - );
129 + const consoleErrorMock = jest.fn();
130 + const consoleWarnMock = jest.fn();
131 + const consoleLogMock = jest.fn();
132
157 - return shouldFilter;
158 - }
133 + global.consoleErrorMock = consoleErrorMock;
134 + global.consoleWarnMock = consoleWarnMock;
135 + global.consoleLogMock = consoleLogMock;
136
160 - const originalConsoleError = console.error;
137 console.error = (...args) => {
138 let firstArg = args[0];
139 if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) {
@@ -184,17 +160,68 @@ beforeEach(() => {
160 // Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
161 return;
162 }
163 +
164 + consoleErrorMock(...args);
165 originalConsoleError.apply(console, args);
166 };
189 - const originalConsoleWarn = console.warn;
167 console.warn = (...args) => {
168 if (shouldIgnoreConsoleErrorOrWarn(args)) {
169 // Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
170 // Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
171 return;
172 }
173 +
174 + consoleWarnMock(...args);
175 originalConsoleWarn.apply(console, args);
176 };
177 + console.log = (...args) => {
178 + consoleLogMock(...args);
179 + originalConsoleLog.apply(console, args);
180 + };
181 +}
182 +
183 +function unpatchConsoleAfterTesting() {
184 + delete global.consoleErrorMock;
185 + delete global.consoleWarnMock;
186 + delete global.consoleLogMock;
187 +}
188 +
189 +beforeEach(() => {
190 + patchConsoleForTestingBeforeHookInstallation();
191 +
192 + global.mockClipboardCopy = jest.fn();
193 +
194 + // Test environment doesn't support document methods like execCommand()
195 + // Also once the backend components below have been required,
196 + // it's too late for a test to mock the clipboard-js modules.
197 + jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
198 +
199 + // These files should be required (and re-required) before each test,
200 + // rather than imported at the head of the module.
201 + // That's because we reset modules between tests,
202 + // which disconnects the DevTool's cache from the current dispatcher ref.
203 + const Agent = require('react-devtools-shared/src/backend/agent').default;
204 + const {initBackend} = require('react-devtools-shared/src/backend');
205 + const Bridge = require('react-devtools-shared/src/bridge').default;
206 + const Store = require('react-devtools-shared/src/devtools/store').default;
207 + const {installHook} = require('react-devtools-shared/src/hook');
208 + const {
209 + getDefaultComponentFilters,
210 + setSavedComponentFilters,
211 + } = require('react-devtools-shared/src/utils');
212 +
213 + // Fake timers let us flush Bridge operations between setup and assertions.
214 + jest.useFakeTimers();
215 +
216 + // We use fake timers heavily in tests but the bridge batching now uses microtasks.
217 + global.devtoolsJestTestScheduler = callback => {
218 + setTimeout(callback, 0);
219 + };
220 +
221 + // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
222 + global._ignoredErrorOrWarningMessages = [
223 + 'react-test-renderer is deprecated.',
224 + ];
225
226 // Initialize filters to a known good state.
227 setSavedComponentFilters(getDefaultComponentFilters());
@@ -203,7 +230,12 @@ beforeEach(() => {
230 // Also initialize inline warnings so that we can test them.
231 global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
232
206 - installHook(global);
233 + installHook(global, {
234 + appendComponentStack: true,
235 + breakOnConsoleErrors: false,
236 + showInlineWarningsAndErrors: true,
237 + hideConsoleLogsInStrictMode: false,
238 + });
239
240 const bridgeListeners = [];
241 const bridge = new Bridge({
@@ -221,14 +253,12 @@ beforeEach(() => {
253 },
254 });
255
224 - const agent = new Agent(((bridge: any): BackendBridge));
256 + const store = new Store(((bridge: any): FrontendBridge));
257
258 + const agent = new Agent(((bridge: any): BackendBridge));
259 const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
227 -
260 initBackend(hook, agent, global);
261
230 - const store = new Store(((bridge: any): FrontendBridge));
231 -
262 global.agent = agent;
263 global.bridge = bridge;
264 global.store = store;
@@ -243,8 +273,10 @@ beforeEach(() => {
273 }
274 global.fetch = mockFetch;
275 });
276 +
277 afterEach(() => {
278 delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
279 + unpatchConsoleAfterTesting();
280
281 // It's important to reset modules between test runs;
282 // Without this, ReactDOM won't re-inject itself into the new hook.
packages/react-devtools-shared/src/backend/agent.js
+1 -9
@@ -24,7 +24,6 @@ import {
24 initialize as setupTraceUpdates,
25 toggleEnabled as setTraceUpdatesEnabled,
26 } from './views/TraceUpdates';
27 -import {patch as patchConsole} from './console';
27 import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
28
29 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -36,7 +35,6 @@ import type {
35 PathMatch,
36 RendererID,
37 RendererInterface,
39 - ConsolePatchSettings,
38 DevToolsHookSettings,
39 } from './types';
40 import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
@@ -805,7 +803,7 @@ export default class Agent extends EventEmitter<{
803 };
804
805 updateConsolePatchSettings: (
808 - settings: $ReadOnly<ConsolePatchSettings>,
806 + settings: $ReadOnly<DevToolsHookSettings>,
807 ) => void = settings => {
808 // Propagate the settings, so Backend can subscribe to it and modify hook
809 this.emit('updateHookSettings', {
@@ -814,12 +812,6 @@ export default class Agent extends EventEmitter<{
812 showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
813 hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
814 });
817 -
818 - // If the frontend preferences have changed,
819 - // or in the case of React Native- if the backend is just finding out the preferences-
820 - // then reinstall the console overrides.
821 - // It's safe to call `patchConsole` multiple times.
822 - patchConsole(settings);
815 };
816
817 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
packages/react-devtools-shared/src/backend/console.js
+1 -417
@@ -7,416 +7,7 @@
7 * @flow
8 */
9
10 -import type {
11 - ConsolePatchSettings,
12 - OnErrorOrWarning,
13 - GetComponentStack,
14 -} from './types';
15 -
16 -import {
17 - formatConsoleArguments,
18 - formatWithStyles,
19 -} from 'react-devtools-shared/src/backend/utils';
20 -import {
21 - FIREFOX_CONSOLE_DIMMING_COLOR,
22 - ANSI_STYLE_DIMMING_TEMPLATE,
23 - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
24 -} from 'react-devtools-shared/src/constants';
25 -import {castBool} from '../utils';
26 -
27 -const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
28 -
29 -// React's custom built component stack strings match "\s{4}in"
30 -// Chrome's prefix matches "\s{4}at"
31 -const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
32 -// Firefox and Safari have no prefix ("")
33 -// but we can fallback to looking for location info (e.g. "foo.js:12:345")
34 -const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
35 -
36 -export function isStringComponentStack(text: string): boolean {
37 - return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
38 -}
39 -
40 -const STYLE_DIRECTIVE_REGEX = /^%c/;
41 -
42 -// This function tells whether or not the arguments for a console
43 -// method has been overridden by the patchForStrictMode function.
44 -// If it has we'll need to do some special formatting of the arguments
45 -// so the console color stays consistent
46 -function isStrictModeOverride(args: Array<any>): boolean {
47 - if (__IS_FIREFOX__) {
48 - return (
49 - args.length >= 2 &&
50 - STYLE_DIRECTIVE_REGEX.test(args[0]) &&
51 - args[1] === FIREFOX_CONSOLE_DIMMING_COLOR
52 - );
53 - } else {
54 - return args.length >= 2 && args[0] === ANSI_STYLE_DIMMING_TEMPLATE;
55 - }
56 -}
57 -
58 -// We add a suffix to some frames that older versions of React didn't do.
59 -// To compare if it's equivalent we strip out the suffix to see if they're
60 -// still equivalent. Similarly, we sometimes use [] and sometimes () so we
61 -// strip them to for the comparison.
62 -const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
63 -function areStackTracesEqual(a: string, b: string): boolean {
64 - return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
65 -}
66 -
67 -function restorePotentiallyModifiedArgs(args: Array<any>): Array<any> {
68 - // If the arguments don't have any styles applied, then just copy
69 - if (!isStrictModeOverride(args)) {
70 - return args.slice();
71 - }
72 -
73 - if (__IS_FIREFOX__) {
74 - // Filter out %c from the start of the first argument and color as a second argument
75 - return [args[0].slice(2)].concat(args.slice(2));
76 - } else {
77 - // Filter out the `\x1b...%s\x1b` template
78 - return args.slice(1);
79 - }
80 -}
81 -
82 -const injectedRenderers: Array<{
83 - onErrorOrWarning: ?OnErrorOrWarning,
84 - getComponentStack: ?GetComponentStack,
85 -}> = [];
86 -
87 -let targetConsole: Object = console;
88 -let targetConsoleMethods: {[string]: $FlowFixMe} = {};
89 -for (const method in console) {
90 - // $FlowFixMe[invalid-computed-prop]
91 - targetConsoleMethods[method] = console[method];
92 -}
93 -
94 -let unpatchFn: null | (() => void) = null;
95 -
96 -// Enables e.g. Jest tests to inject a mock console object.
97 -export function dangerous_setTargetConsoleForTesting(
98 - targetConsoleForTesting: Object,
99 -): void {
100 - targetConsole = targetConsoleForTesting;
101 -
102 - targetConsoleMethods = ({}: {[string]: $FlowFixMe});
103 - for (const method in targetConsole) {
104 - // $FlowFixMe[invalid-computed-prop]
105 - targetConsoleMethods[method] = console[method];
106 - }
107 -}
108 -
109 -// v16 renderers should use this method to inject internals necessary to generate a component stack.
110 -// These internals will be used if the console is patched.
111 -// Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
112 -export function registerRenderer(
113 - onErrorOrWarning?: OnErrorOrWarning,
114 - getComponentStack?: GetComponentStack,
115 -): void {
116 - injectedRenderers.push({
117 - onErrorOrWarning,
118 - getComponentStack,
119 - });
120 -}
121 -
122 -const consoleSettingsRef: ConsolePatchSettings = {
123 - appendComponentStack: false,
124 - breakOnConsoleErrors: false,
125 - showInlineWarningsAndErrors: false,
126 - hideConsoleLogsInStrictMode: false,
127 -};
128 -
129 -// Patches console methods to append component stack for the current fiber.
130 -// Call unpatch() to remove the injected behavior.
131 -export function patch({
132 - appendComponentStack,
133 - breakOnConsoleErrors,
134 - showInlineWarningsAndErrors,
135 - hideConsoleLogsInStrictMode,
136 -}: $ReadOnly<ConsolePatchSettings>): void {
137 - // Settings may change after we've patched the console.
138 - // Using a shared ref allows the patch function to read the latest values.
139 - consoleSettingsRef.appendComponentStack = appendComponentStack;
140 - consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
141 - consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
142 - consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
143 -
144 - if (
145 - appendComponentStack ||
146 - breakOnConsoleErrors ||
147 - showInlineWarningsAndErrors
148 - ) {
149 - if (unpatchFn !== null) {
150 - // Don't patch twice.
151 - return;
152 - }
153 -
154 - const originalConsoleMethods: {[string]: $FlowFixMe} = {};
155 -
156 - unpatchFn = () => {
157 - for (const method in originalConsoleMethods) {
158 - try {
159 - targetConsole[method] = originalConsoleMethods[method];
160 - } catch (error) {}
161 - }
162 - };
163 -
164 - OVERRIDE_CONSOLE_METHODS.forEach(method => {
165 - try {
166 - const originalMethod = (originalConsoleMethods[method] = targetConsole[
167 - method
168 - ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
169 - ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
170 - : targetConsole[method]);
171 -
172 - // $FlowFixMe[missing-local-annot]
173 - const overrideMethod = (...args) => {
174 - let alreadyHasComponentStack = false;
175 - if (method !== 'log' && consoleSettingsRef.appendComponentStack) {
176 - const lastArg = args.length > 0 ? args[args.length - 1] : null;
177 - alreadyHasComponentStack =
178 - typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
179 - }
180 -
181 - const shouldShowInlineWarningsAndErrors =
182 - consoleSettingsRef.showInlineWarningsAndErrors &&
183 - (method === 'error' || method === 'warn');
184 -
185 - // Search for the first renderer that has a current Fiber.
186 - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
187 - for (let i = 0; i < injectedRenderers.length; i++) {
188 - const renderer = injectedRenderers[i];
189 - const {getComponentStack, onErrorOrWarning} = renderer;
190 - try {
191 - if (shouldShowInlineWarningsAndErrors) {
192 - // patch() is called by two places: (1) the hook and (2) the renderer backend.
193 - // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
194 - if (onErrorOrWarning != null) {
195 - onErrorOrWarning(
196 - ((method: any): 'error' | 'warn'),
197 - // Restore and copy args before we mutate them (e.g. adding the component stack)
198 - restorePotentiallyModifiedArgs(args),
199 - );
200 - }
201 - }
202 - } catch (error) {
203 - // Don't let a DevTools or React internal error interfere with logging.
204 - setTimeout(() => {
205 - throw error;
206 - }, 0);
207 - }
208 - try {
209 - if (
210 - consoleSettingsRef.appendComponentStack &&
211 - getComponentStack != null
212 - ) {
213 - // This needs to be directly in the wrapper so we can pop exactly one frame.
214 - const topFrame = Error('react-stack-top-frame');
215 - const match = getComponentStack(topFrame);
216 - if (match !== null) {
217 - const {enableOwnerStacks, componentStack} = match;
218 - // Empty string means we have a match but no component stack.
219 - // We don't need to look in other renderers but we also don't add anything.
220 - if (componentStack !== '') {
221 - // Create a fake Error so that when we print it we get native source maps. Every
222 - // browser will print the .stack property of the error and then parse it back for source
223 - // mapping. Rather than print the internal slot. So it doesn't matter that the internal
224 - // slot doesn't line up.
225 - const fakeError = new Error('');
226 - // In Chromium, only the stack property is printed but in Firefox the <name>:<message>
227 - // gets printed so to make the colon make sense, we name it so we print Stack:
228 - // and similarly Safari leave an expandable slot.
229 - if (__IS_CHROME__ || __IS_EDGE__) {
230 - // Before sending the stack to Chrome DevTools for formatting,
231 - // V8 will reconstruct this according to the template <name>: <message><stack-frames>
232 - // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
233 - // It has to start with ^[\w.]*Error\b to trigger stack formatting.
234 - fakeError.name = enableOwnerStacks
235 - ? 'Error Stack'
236 - : 'Error Component Stack'; // This gets printed
237 - } else {
238 - fakeError.name = enableOwnerStacks
239 - ? 'Stack'
240 - : 'Component Stack'; // This gets printed
241 - }
242 - // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
243 - // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
244 - // to our own stack.
245 - fakeError.stack =
246 - __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
247 - ? (enableOwnerStacks
248 - ? 'Error Stack:'
249 - : 'Error Component Stack:') + componentStack
250 - : componentStack;
251 -
252 - if (alreadyHasComponentStack) {
253 - // Only modify the component stack if it matches what we would've added anyway.
254 - // Otherwise we assume it was a non-React stack.
255 - if (isStrictModeOverride(args)) {
256 - // We do nothing to Strict Mode overrides that already has a stack
257 - // because we have already lost some context for how to format it
258 - // since we've already merged the stack into the log at this point.
259 - } else if (
260 - areStackTracesEqual(
261 - args[args.length - 1],
262 - componentStack,
263 - )
264 - ) {
265 - const firstArg = args[0];
266 - if (
267 - args.length > 1 &&
268 - typeof firstArg === 'string' &&
269 - firstArg.endsWith('%s')
270 - ) {
271 - args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
272 - }
273 - args[args.length - 1] = fakeError;
274 - }
275 - } else {
276 - args.push(fakeError);
277 - if (isStrictModeOverride(args)) {
278 - if (__IS_FIREFOX__) {
279 - args[0] = `${args[0]} %o`;
280 - } else {
281 - args[0] =
282 - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
283 - }
284 - }
285 - }
286 - }
287 - // Don't add stacks from other renderers.
288 - break;
289 - }
290 - }
291 - } catch (error) {
292 - // Don't let a DevTools or React internal error interfere with logging.
293 - setTimeout(() => {
294 - throw error;
295 - }, 0);
296 - }
297 - }
298 -
299 - if (consoleSettingsRef.breakOnConsoleErrors) {
300 - // --- Welcome to debugging with React DevTools ---
301 - // This debugger statement means that you've enabled the "break on warnings" feature.
302 - // Use the browser's Call Stack panel to step out of this override function-
303 - // to where the original warning or error was logged.
304 - // eslint-disable-next-line no-debugger
305 - debugger;
306 - }
307 -
308 - originalMethod(...args);
309 - };
310 -
311 - overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
312 - originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
313 -
314 - targetConsole[method] = overrideMethod;
315 - } catch (error) {}
316 - });
317 - } else {
318 - unpatch();
319 - }
320 -}
321 -
322 -// Removed component stack patch from console methods.
323 -export function unpatch(): void {
324 - if (unpatchFn !== null) {
325 - unpatchFn();
326 - unpatchFn = null;
327 - }
328 -}
329 -
330 -let unpatchForStrictModeFn: null | (() => void) = null;
331 -
332 -// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode
333 -export function patchForStrictMode() {
334 - const overrideConsoleMethods = [
335 - 'error',
336 - 'group',
337 - 'groupCollapsed',
338 - 'info',
339 - 'log',
340 - 'trace',
341 - 'warn',
342 - ];
343 -
344 - if (unpatchForStrictModeFn !== null) {
345 - // Don't patch twice.
346 - return;
347 - }
348 -
349 - const originalConsoleMethods: {[string]: $FlowFixMe} = {};
350 -
351 - unpatchForStrictModeFn = () => {
352 - for (const method in originalConsoleMethods) {
353 - try {
354 - targetConsole[method] = originalConsoleMethods[method];
355 - } catch (error) {}
356 - }
357 - };
358 -
359 - overrideConsoleMethods.forEach(method => {
360 - try {
361 - const originalMethod = (originalConsoleMethods[method] = targetConsole[
362 - method
363 - ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
364 - ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
365 - : targetConsole[method]);
366 -
367 - // $FlowFixMe[missing-local-annot]
368 - const overrideMethod = (...args) => {
369 - if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
370 - // Dim the text color of the double logs if we're not hiding them.
371 - if (__IS_FIREFOX__) {
372 - originalMethod(
373 - ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
374 - );
375 - } else {
376 - originalMethod(
377 - ANSI_STYLE_DIMMING_TEMPLATE,
378 - ...formatConsoleArguments(...args),
379 - );
380 - }
381 - }
382 - };
383 -
384 - overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
385 - originalMethod;
386 - originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
387 - overrideMethod;
388 -
389 - targetConsole[method] = overrideMethod;
390 - } catch (error) {}
391 - });
392 -}
393 -
394 -// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode
395 -export function unpatchForStrictMode(): void {
396 - if (unpatchForStrictModeFn !== null) {
397 - unpatchForStrictModeFn();
398 - unpatchForStrictModeFn = null;
399 - }
400 -}
401 -
402 -export function patchConsoleUsingWindowValues() {
403 - const appendComponentStack =
404 - castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true;
405 - const breakOnConsoleErrors =
406 - castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false;
407 - const showInlineWarningsAndErrors =
408 - castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ?? true;
409 - const hideConsoleLogsInStrictMode =
410 - castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ??
411 - false;
412 -
413 - patch({
414 - appendComponentStack,
415 - breakOnConsoleErrors,
416 - showInlineWarningsAndErrors,
417 - hideConsoleLogsInStrictMode,
418 - });
419 -}
10 +import type {ConsolePatchSettings} from './types';
11
12 // After receiving cached console patch settings from React Native, we set them on window.
13 // When the console is initially patched (in renderer.js and hook.js), these values are read.
@@ -433,10 +24,3 @@ export function writeConsolePatchSettingsToWindow(
24 window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
25 settings.hideConsoleLogsInStrictMode;
26 }
436 -
437 -export function installConsoleFunctionsToWindow(): void {
438 - window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
439 - patchConsoleUsingWindowValues,
440 - registerRendererWithConsole: registerRenderer,
441 - };
442 -}
packages/react-devtools-shared/src/backend/fiber/renderer.js
-18
@@ -71,12 +71,6 @@ import {
71 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
72 } from '../../constants';
73 import {inspectHooksOfFiber} from 'react-debug-tools';
74 -import {
75 - patchConsoleUsingWindowValues,
76 - registerRenderer as registerRendererWithConsole,
77 - patchForStrictMode as patchConsoleForStrictMode,
78 - unpatchForStrictMode as unpatchConsoleForStrictMode,
79 -} from '../console';
74 import {
75 CONCURRENT_MODE_NUMBER,
76 CONCURRENT_MODE_SYMBOL_STRING,
@@ -1198,16 +1192,6 @@ export function attach(
1192 needsToFlushComponentLogs = true;
1193 }
1194
1201 - // Patching the console enables DevTools to do a few useful things:
1202 - // * Append component stacks to warnings and error messages
1203 - // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
1204 - registerRendererWithConsole(onErrorOrWarning, getComponentStack);
1205 -
1206 - // The renderer interface can't read these preferences directly,
1207 - // because it is stored in localStorage within the context of the extension.
1208 - // It relies on the extension to pass the preference through via the global.
1209 - patchConsoleUsingWindowValues();
1210 -
1195 function debug(
1196 name: string,
1197 instance: DevToolsInstance,
@@ -5788,7 +5772,6 @@ export function attach(
5772 hasElementWithId,
5773 inspectElement,
5774 logElementToConsole,
5791 - patchConsoleForStrictMode,
5775 getComponentStack,
5776 getElementAttributeByPath,
5777 getElementSourceFunctionById,
@@ -5803,7 +5786,6 @@ export function attach(
5786 startProfiling,
5787 stopProfiling,
5788 storeAsGlobal,
5806 - unpatchConsoleForStrictMode,
5789 updateComponentFilters,
5790 getEnvironmentNames,
5791 };
packages/react-devtools-shared/src/backend/flight/renderer.js
-10
@@ -19,11 +19,6 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
19
20 import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils';
21
22 -import {
23 - patchConsoleUsingWindowValues,
24 - registerRenderer as registerRendererWithConsole,
25 -} from '../console';
26 -
22 function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
23 // If this ReactComponentInfo supports native console.createTask then we are already running
24 // inside a native async stack trace if it's active - meaning the DevTools is open.
@@ -145,9 +140,6 @@ export function attach(
140 // The changes will be flushed later when we commit this tree to Fiber.
141 }
142
148 - patchConsoleUsingWindowValues();
149 - registerRendererWithConsole(onErrorOrWarning, getComponentStack);
150 -
143 return {
144 cleanup() {},
145 clearErrorsAndWarnings() {},
@@ -205,7 +197,6 @@ export function attach(
197 };
198 },
199 logElementToConsole() {},
208 - patchConsoleForStrictMode() {},
200 getElementAttributeByPath() {},
201 getElementSourceFunctionById() {},
202 onErrorOrWarning,
@@ -219,7 +210,6 @@ export function attach(
210 startProfiling() {},
211 stopProfiling() {},
212 storeAsGlobal() {},
222 - unpatchConsoleForStrictMode() {},
213 updateComponentFilters() {},
214 getEnvironmentNames() {
215 return [];
packages/react-devtools-shared/src/backend/legacy/renderer.js
-6
@@ -1103,10 +1103,6 @@ export function attach(
1103 // Not implemented
1104 }
1105
1106 - function patchConsoleForStrictMode() {}
1107 -
1108 - function unpatchConsoleForStrictMode() {}
1109 -
1106 function hasElementWithId(id: number): boolean {
1107 return idToInternalInstanceMap.has(id);
1108 }
@@ -1141,7 +1137,6 @@ export function attach(
1137 overrideSuspense,
1138 overrideValueAtPath,
1139 renamePath,
1144 - patchConsoleForStrictMode,
1140 getElementAttributeByPath,
1141 getElementSourceFunctionById,
1142 renderer,
@@ -1150,7 +1145,6 @@ export function attach(
1145 startProfiling,
1146 stopProfiling,
1147 storeAsGlobal,
1153 - unpatchConsoleForStrictMode,
1148 updateComponentFilters,
1149 getEnvironmentNames,
1150 };
packages/react-devtools-shared/src/backend/types.js
-2
@@ -404,7 +404,6 @@ export type RendererInterface = {
404 path: Array<string | number>,
405 value: any,
406 ) => void,
407 - patchConsoleForStrictMode: () => void,
407 getElementAttributeByPath: (
408 id: number,
409 path: Array<string | number>,
@@ -427,7 +426,6 @@ export type RendererInterface = {
426 path: Array<string | number>,
427 count: number,
428 ) => void,
430 - unpatchConsoleForStrictMode: () => void,
429 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
430 getEnvironmentNames: () => Array<string>,
431
packages/react-devtools-shared/src/bridge.js
+2 -2
@@ -15,7 +15,7 @@ import type {
15 OwnersList,
16 ProfilingDataBackend,
17 RendererID,
18 - ConsolePatchSettings,
18 + DevToolsHookSettings,
19 } from 'react-devtools-shared/src/backend/types';
20 import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
21
@@ -241,7 +241,7 @@ type FrontendEvents = {
241 storeAsGlobal: [StoreAsGlobalParams],
242 updateComponentFilters: [Array<ComponentFilter>],
243 getEnvironmentNames: [],
244 - updateConsolePatchSettings: [ConsolePatchSettings],
244 + updateConsolePatchSettings: [DevToolsHookSettings],
245 viewAttributeSource: [ViewAttributeSourceParams],
246 viewElementSource: [ElementAndRendererID],
247
packages/react-devtools-shared/src/hook.js
+282 -126
@@ -21,10 +21,31 @@ import type {
21 import {
22 FIREFOX_CONSOLE_DIMMING_COLOR,
23 ANSI_STYLE_DIMMING_TEMPLATE,
24 + ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
25 } from 'react-devtools-shared/src/constants';
26 import attachRenderer from './attachRenderer';
27
27 -declare var window: any;
28 +// React's custom built component stack strings match "\s{4}in"
29 +// Chrome's prefix matches "\s{4}at"
30 +const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
31 +// Firefox and Safari have no prefix ("")
32 +// but we can fallback to looking for location info (e.g. "foo.js:12:345")
33 +const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
34 +
35 +function isStringComponentStack(text: string): boolean {
36 + return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
37 +}
38 +
39 +// We add a suffix to some frames that older versions of React didn't do.
40 +// To compare if it's equivalent we strip out the suffix to see if they're
41 +// still equivalent. Similarly, we sometimes use [] and sometimes () so we
42 +// strip them to for the comparison.
43 +const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
44 +function areStackTracesEqual(a: string, b: string): boolean {
45 + return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
46 +}
47 +
48 +const targetConsole: Object = console;
49
50 export function installHook(
51 target: any,
@@ -36,25 +57,6 @@ export function installHook(
57 return null;
58 }
59
39 - let targetConsole: Object = console;
40 - let targetConsoleMethods: {[string]: $FlowFixMe} = {};
41 - for (const method in console) {
42 - // $FlowFixMe[invalid-computed-prop]
43 - targetConsoleMethods[method] = console[method];
44 - }
45 -
46 - function dangerous_setTargetConsoleForTesting(
47 - targetConsoleForTesting: Object,
48 - ): void {
49 - targetConsole = targetConsoleForTesting;
50 -
51 - targetConsoleMethods = ({}: {[string]: $FlowFixMe});
52 - for (const method in targetConsole) {
53 - // $FlowFixMe[invalid-computed-prop]
54 - targetConsoleMethods[method] = console[method];
55 - }
56 - }
57 -
60 function detectReactBuildType(renderer: ReactRenderer) {
61 try {
62 if (typeof renderer.version === 'string') {
@@ -189,10 +191,7 @@ export function installHook(
191 }
192
193 // NOTE: KEEP IN SYNC with src/backend/utils.js
192 - function formatWithStyles(
193 - inputArgs: $ReadOnlyArray<any>,
194 - style?: string,
195 - ): $ReadOnlyArray<any> {
194 + function formatWithStyles(inputArgs: Array<any>, style?: string): Array<any> {
195 if (
196 inputArgs === undefined ||
197 inputArgs === null ||
@@ -285,85 +284,6 @@ export function installHook(
284 return [template, ...args];
285 }
286
288 - let unpatchFn = null;
289 -
290 - // NOTE: KEEP IN SYNC with src/backend/console.js:patchForStrictMode
291 - // This function hides or dims console logs during the initial double renderer
292 - // in Strict Mode. We need this function because during initial render,
293 - // React and DevTools are connecting and the renderer interface isn't avaiable
294 - // and we want to be able to have consistent logging behavior for double logs
295 - // during the initial renderer.
296 - function patchConsoleForInitialCommitInStrictMode(
297 - hideConsoleLogsInStrictMode: boolean,
298 - ) {
299 - const overrideConsoleMethods = [
300 - 'error',
301 - 'group',
302 - 'groupCollapsed',
303 - 'info',
304 - 'log',
305 - 'trace',
306 - 'warn',
307 - ];
308 -
309 - if (unpatchFn !== null) {
310 - // Don't patch twice.
311 - return;
312 - }
313 -
314 - const originalConsoleMethods: {[string]: $FlowFixMe} = {};
315 -
316 - unpatchFn = () => {
317 - for (const method in originalConsoleMethods) {
318 - try {
319 - targetConsole[method] = originalConsoleMethods[method];
320 - } catch (error) {}
321 - }
322 - };
323 -
324 - overrideConsoleMethods.forEach(method => {
325 - try {
326 - const originalMethod = (originalConsoleMethods[method] = targetConsole[
327 - method
328 - ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
329 - ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
330 - : targetConsole[method]);
331 -
332 - const overrideMethod = (...args: $ReadOnlyArray<any>) => {
333 - // Dim the text color of the double logs if we're not hiding them.
334 - if (!hideConsoleLogsInStrictMode) {
335 - // Firefox doesn't support ANSI escape sequences
336 - if (__IS_FIREFOX__) {
337 - originalMethod(
338 - ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
339 - );
340 - } else {
341 - originalMethod(
342 - ANSI_STYLE_DIMMING_TEMPLATE,
343 - ...formatConsoleArguments(...args),
344 - );
345 - }
346 - }
347 - };
348 -
349 - overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
350 - originalMethod;
351 - originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
352 - overrideMethod;
353 -
354 - targetConsole[method] = overrideMethod;
355 - } catch (error) {}
356 - });
357 - }
358 -
359 - // NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
360 - function unpatchConsoleForInitialCommitInStrictMode() {
361 - if (unpatchFn !== null) {
362 - unpatchFn();
363 - unpatchFn = null;
364 - }
365 - }
366 -
287 let uidCounter = 0;
288 function inject(renderer: ReactRenderer): number {
289 const id = ++uidCounter;
@@ -469,28 +389,85 @@ export function installHook(
389 }
390 }
391
472 - function setStrictMode(rendererID: RendererID, isStrictMode: any) {
473 - const rendererInterface = rendererInterfaces.get(rendererID);
474 - if (rendererInterface != null) {
475 - if (isStrictMode) {
476 - rendererInterface.patchConsoleForStrictMode();
477 - } else {
478 - rendererInterface.unpatchConsoleForStrictMode();
479 - }
392 + let isRunningDuringStrictModeInvocation = false;
393 + function setStrictMode(rendererID: RendererID, isStrictMode: boolean) {
394 + isRunningDuringStrictModeInvocation = isStrictMode;
395 +
396 + if (isStrictMode) {
397 + patchConsoleForStrictMode();
398 } else {
481 - // This should only happen during initial commit in the extension before DevTools
482 - // finishes its handshake with the injected renderer
483 - if (isStrictMode) {
484 - const hideConsoleLogsInStrictMode =
485 - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
486 -
487 - patchConsoleForInitialCommitInStrictMode(hideConsoleLogsInStrictMode);
488 - } else {
489 - unpatchConsoleForInitialCommitInStrictMode();
490 - }
399 + unpatchConsoleForStrictMode();
400 + }
401 + }
402 +
403 + const unpatchConsoleCallbacks = [];
404 + // For StrictMode we patch console once we are running in StrictMode and unpatch right after it
405 + // So patching could happen multiple times during the runtime
406 + // Notice how we don't patch error or warn methods, because they are already patched in patchConsoleForErrorsAndWarnings
407 + // This will only happen once, when hook is installed
408 + function patchConsoleForStrictMode() {
409 + // Don't patch console in case settings were not injected
410 + if (!hook.settings) {
411 + return;
412 + }
413 +
414 + // Don't patch twice
415 + if (unpatchConsoleCallbacks.length > 0) {
416 + return;
417 + }
418 +
419 + // At this point 'error', 'warn', and 'trace' methods are already patched
420 + // by React DevTools hook to append component stacks and other possible features.
421 + const consoleMethodsToOverrideForStrictMode = [
422 + 'group',
423 + 'groupCollapsed',
424 + 'info',
425 + 'log',
426 + ];
427 +
428 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
429 + for (const method of consoleMethodsToOverrideForStrictMode) {
430 + const originalMethod = targetConsole[method];
431 + const overrideMethod: (...args: Array<any>) => void = (
432 + ...args: any[]
433 + ) => {
434 + const settings = hook.settings;
435 + // Something unexpected happened, fallback to just printing the console message.
436 + if (settings == null) {
437 + originalMethod(...args);
438 + return;
439 + }
440 +
441 + if (settings.hideConsoleLogsInStrictMode) {
442 + return;
443 + }
444 +
445 + // Dim the text color of the double logs if we're not hiding them.
446 + // Firefox doesn't support ANSI escape sequences
447 + if (__IS_FIREFOX__) {
448 + originalMethod(
449 + ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
450 + );
451 + } else {
452 + originalMethod(
453 + ANSI_STYLE_DIMMING_TEMPLATE,
454 + ...formatConsoleArguments(...args),
455 + );
456 + }
457 + };
458 +
459 + targetConsole[method] = overrideMethod;
460 + unpatchConsoleCallbacks.push(() => {
461 + targetConsole[method] = originalMethod;
462 + });
463 }
464 }
465
466 + function unpatchConsoleForStrictMode() {
467 + unpatchConsoleCallbacks.forEach(callback => callback());
468 + unpatchConsoleCallbacks.length = 0;
469 + }
470 +
471 type StackFrameString = string;
472
473 const openModuleRangesStack: Array<StackFrameString> = [];
@@ -526,6 +503,188 @@ export function installHook(
503 }
504 }
505
506 + // For Errors and Warnings we only patch console once
507 + function patchConsoleForErrorsAndWarnings() {
508 + // Don't patch console in case settings were not injected
509 + if (!hook.settings) {
510 + return;
511 + }
512 +
513 + const consoleMethodsToOverrideForErrorsAndWarnings = [
514 + 'error',
515 + 'trace',
516 + 'warn',
517 + ];
518 +
519 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
520 + for (const method of consoleMethodsToOverrideForErrorsAndWarnings) {
521 + const originalMethod = targetConsole[method];
522 + const overrideMethod: (...args: Array<any>) => void = (...args) => {
523 + const settings = hook.settings;
524 + // Something unexpected happened, fallback to just printing the console message.
525 + if (settings == null) {
526 + originalMethod(...args);
527 + return;
528 + }
529 +
530 + if (
531 + isRunningDuringStrictModeInvocation &&
532 + settings.hideConsoleLogsInStrictMode
533 + ) {
534 + return;
535 + }
536 +
537 + let injectedComponentStackAsFakeError = false;
538 + let alreadyHasComponentStack = false;
539 + if (settings.appendComponentStack) {
540 + const lastArg = args.length > 0 ? args[args.length - 1] : null;
541 + alreadyHasComponentStack =
542 + typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
543 + }
544 +
545 + const shouldShowInlineWarningsAndErrors =
546 + settings.showInlineWarningsAndErrors &&
547 + (method === 'error' || method === 'warn');
548 +
549 + // Search for the first renderer that has a current Fiber.
550 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
551 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
552 + for (const rendererInterface of hook.rendererInterfaces.values()) {
553 + const {onErrorOrWarning, getComponentStack} = rendererInterface;
554 + try {
555 + if (shouldShowInlineWarningsAndErrors) {
556 + // patch() is called by two places: (1) the hook and (2) the renderer backend.
557 + // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
558 + if (onErrorOrWarning != null) {
559 + onErrorOrWarning(
560 + ((method: any): 'error' | 'warn'),
561 + args.slice(),
562 + );
563 + }
564 + }
565 + } catch (error) {
566 + // Don't let a DevTools or React internal error interfere with logging.
567 + setTimeout(() => {
568 + throw error;
569 + }, 0);
570 + }
571 +
572 + try {
573 + if (settings.appendComponentStack && getComponentStack != null) {
574 + // This needs to be directly in the wrapper so we can pop exactly one frame.
575 + const topFrame = Error('react-stack-top-frame');
576 + const match = getComponentStack(topFrame);
577 + if (match !== null) {
578 + const {enableOwnerStacks, componentStack} = match;
579 + // Empty string means we have a match but no component stack.
580 + // We don't need to look in other renderers but we also don't add anything.
581 + if (componentStack !== '') {
582 + // Create a fake Error so that when we print it we get native source maps. Every
583 + // browser will print the .stack property of the error and then parse it back for source
584 + // mapping. Rather than print the internal slot. So it doesn't matter that the internal
585 + // slot doesn't line up.
586 + const fakeError = new Error('');
587 + // In Chromium, only the stack property is printed but in Firefox the <name>:<message>
588 + // gets printed so to make the colon make sense, we name it so we print Stack:
589 + // and similarly Safari leave an expandable slot.
590 + if (__IS_CHROME__ || __IS_EDGE__) {
591 + // Before sending the stack to Chrome DevTools for formatting,
592 + // V8 will reconstruct this according to the template <name>: <message><stack-frames>
593 + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
594 + // It has to start with ^[\w.]*Error\b to trigger stack formatting.
595 + fakeError.name = enableOwnerStacks
596 + ? 'Error Stack'
597 + : 'Error Component Stack'; // This gets printed
598 + } else {
599 + fakeError.name = enableOwnerStacks
600 + ? 'Stack'
601 + : 'Component Stack'; // This gets printed
602 + }
603 + // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
604 + // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
605 + // to our own stack.
606 + fakeError.stack =
607 + __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
608 + ? (enableOwnerStacks
609 + ? 'Error Stack:'
610 + : 'Error Component Stack:') + componentStack
611 + : componentStack;
612 +
613 + if (alreadyHasComponentStack) {
614 + // Only modify the component stack if it matches what we would've added anyway.
615 + // Otherwise we assume it was a non-React stack.
616 + if (
617 + areStackTracesEqual(args[args.length - 1], componentStack)
618 + ) {
619 + const firstArg = args[0];
620 + if (
621 + args.length > 1 &&
622 + typeof firstArg === 'string' &&
623 + firstArg.endsWith('%s')
624 + ) {
625 + args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
626 + }
627 + args[args.length - 1] = fakeError;
628 + injectedComponentStackAsFakeError = true;
629 + }
630 + } else {
631 + args.push(fakeError);
632 + injectedComponentStackAsFakeError = true;
633 + }
634 + }
635 +
636 + // Don't add stacks from other renderers.
637 + break;
638 + }
639 + }
640 + } catch (error) {
641 + // Don't let a DevTools or React internal error interfere with logging.
642 + setTimeout(() => {
643 + throw error;
644 + }, 0);
645 + }
646 + }
647 +
648 + if (settings.breakOnConsoleErrors) {
649 + // --- Welcome to debugging with React DevTools ---
650 + // This debugger statement means that you've enabled the "break on warnings" feature.
651 + // Use the browser's Call Stack panel to step out of this override function
652 + // to where the original warning or error was logged.
653 + // eslint-disable-next-line no-debugger
654 + debugger;
655 + }
656 +
657 + if (isRunningDuringStrictModeInvocation) {
658 + // Dim the text color of the double logs if we're not hiding them.
659 + // Firefox doesn't support ANSI escape sequences
660 + if (__IS_FIREFOX__) {
661 + const argsWithCSSStyles = formatWithStyles(
662 + args,
663 + FIREFOX_CONSOLE_DIMMING_COLOR,
664 + );
665 +
666 + if (injectedComponentStackAsFakeError) {
667 + argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`;
668 + }
669 +
670 + originalMethod(...argsWithCSSStyles);
671 + } else {
672 + originalMethod(
673 + injectedComponentStackAsFakeError
674 + ? ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK
675 + : ANSI_STYLE_DIMMING_TEMPLATE,
676 + ...formatConsoleArguments(...args),
677 + );
678 + }
679 + } else {
680 + originalMethod(...args);
681 + }
682 + };
683 +
684 + targetConsole[method] = overrideMethod;
685 + }
686 + }
687 +
688 // TODO: More meaningful names for "rendererInterfaces" and "renderers".
689 const fiberRoots: {[RendererID]: Set<mixed>} = {};
690 const rendererInterfaces = new Map<RendererID, RendererInterface>();
@@ -580,10 +739,12 @@ export function installHook(
739 showInlineWarningsAndErrors: true,
740 hideConsoleLogsInStrictMode: false,
741 };
742 + patchConsoleForErrorsAndWarnings();
743 } else {
744 Promise.resolve(maybeSettingsOrSettingsPromise)
745 .then(settings => {
746 hook.settings = settings;
747 + patchConsoleForErrorsAndWarnings();
748 })
749 .catch(() => {
750 targetConsole.error(
@@ -592,11 +753,6 @@ export function installHook(
753 });
754 }
755
595 - if (__TEST__) {
596 - hook.dangerous_setTargetConsoleForTesting =
597 - dangerous_setTargetConsoleForTesting;
598 - }
599 -
756 Object.defineProperty(
757 target,
758 '__REACT_DEVTOOLS_GLOBAL_HOOK__',