diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js
index 25001502f1c2b..d588d4f91e9f0 100644
--- a/packages/react-devtools-core/src/backend.js
+++ b/packages/react-devtools-core/src/backend.js
@@ -11,7 +11,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {installHook} from 'react-devtools-shared/src/hook';
import {initBackend} from 'react-devtools-shared/src/backend';
-import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
@@ -41,9 +40,6 @@ type ConnectOptions = {
devToolsSettingsManager: ?DevToolsSettingsManager,
};
-// Install a global variable to allow patching console early (during injection).
-// This provides React Native developers with components stacks even if they don't run DevTools.
-installConsoleFunctionsToWindow();
installHook(window);
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js
index e7d0485b37569..fca1535c4e5ba 100644
--- a/packages/react-devtools-inline/src/backend.js
+++ b/packages/react-devtools-inline/src/backend.js
@@ -3,7 +3,6 @@
import Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {initBackend} from 'react-devtools-shared/src/backend';
-import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
import {installHook} from 'react-devtools-shared/src/hook';
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
@@ -120,8 +119,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
}
export function initialize(contentWindow: any): void {
- // Install a global variable to allow patching console early (during injection).
- // This provides React Native developers with components stacks even if they don't run DevTools.
- installConsoleFunctionsToWindow();
installHook(contentWindow);
}
diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
index b99db5c540097..54af62db44e92 100644
--- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
+++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
@@ -7,27 +7,17 @@
* @flow
*/
-import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils';
+import {
+ getVersionedRenderImplementation,
+ normalizeCodeLocInfo,
+} from 'react-devtools-shared/src/__tests__/utils';
describe('component stack', () => {
let React;
let act;
- let mockError;
- let mockWarn;
let supportsOwnerStacks;
beforeEach(() => {
- // Intercept native console methods before DevTools bootstraps.
- // Normalize component stack locations.
- mockError = jest.fn();
- mockWarn = jest.fn();
- console.error = (...args) => {
- mockError(...args.map(normalizeCodeLocInfo));
- };
- console.warn = (...args) => {
- mockWarn(...args.map(normalizeCodeLocInfo));
- };
-
const utils = require('./utils');
act = utils.act;
@@ -54,18 +44,22 @@ describe('component stack', () => {
act(() => render());
- expect(mockError).toHaveBeenCalledWith(
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
'Test error.',
'\n in Child (at **)' +
'\n in Parent (at **)' +
'\n in Grandparent (at **)',
- );
- expect(mockWarn).toHaveBeenCalledWith(
+ ]);
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
'Test warning.',
'\n in Child (at **)' +
'\n in Parent (at **)' +
'\n in Grandparent (at **)',
- );
+ ]);
});
// This test should have caught #19911
@@ -89,13 +83,15 @@ describe('component stack', () => {
expect(useEffectCount).toBe(1);
- expect(mockWarn).toHaveBeenCalledWith(
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
'Warning to trigger appended component stacks.',
'\n in Example (at **)',
- );
+ ]);
});
- // @reactVersion >=18.3
+ // @reactVersion >= 18.3
it('should log the current component stack with debug info from promises', () => {
const Child = () => {
console.error('Test error.');
@@ -117,23 +113,27 @@ describe('component stack', () => {
act(() => render());
- expect(mockError).toHaveBeenCalledWith(
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
'Test error.',
supportsOwnerStacks
? '\n in Child (at **)'
: '\n in Child (at **)' +
- '\n in ServerComponent (at **)' +
- '\n in Parent (at **)' +
- '\n in Grandparent (at **)',
- );
- expect(mockWarn).toHaveBeenCalledWith(
+ '\n in ServerComponent (at **)' +
+ '\n in Parent (at **)' +
+ '\n in Grandparent (at **)',
+ ]);
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
'Test warning.',
supportsOwnerStacks
? '\n in Child (at **)'
: '\n in Child (at **)' +
- '\n in ServerComponent (at **)' +
- '\n in Parent (at **)' +
- '\n in Grandparent (at **)',
- );
+ '\n in ServerComponent (at **)' +
+ '\n in Parent (at **)' +
+ '\n in Grandparent (at **)',
+ ]);
});
});
diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js
index 516762132e884..00d6d9712679a 100644
--- a/packages/react-devtools-shared/src/__tests__/console-test.js
+++ b/packages/react-devtools-shared/src/__tests__/console-test.js
@@ -7,52 +7,25 @@
* @flow
*/
-import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils';
+import {
+ getVersionedRenderImplementation,
+ normalizeCodeLocInfo,
+} from 'react-devtools-shared/src/__tests__/utils';
let React;
let ReactDOMClient;
let act;
-let fakeConsole;
-let mockError;
-let mockInfo;
-let mockGroup;
-let mockGroupCollapsed;
-let mockLog;
-let mockWarn;
-let patchConsole;
-let unpatchConsole;
let rendererID;
let supportsOwnerStacks = false;
describe('console', () => {
beforeEach(() => {
- const Console = require('react-devtools-shared/src/backend/console');
-
- patchConsole = Console.patch;
- unpatchConsole = Console.unpatch;
-
- // Patch a fake console so we can verify with tests below.
- // Patching the real console is too complicated,
- // because Jest itself has hooks into it as does our test env setup.
- mockError = jest.fn();
- mockInfo = jest.fn();
- mockGroup = jest.fn();
- mockGroupCollapsed = jest.fn();
- mockLog = jest.fn();
- mockWarn = jest.fn();
- fakeConsole = {
- error: mockError,
- info: mockInfo,
- log: mockLog,
- warn: mockWarn,
- group: mockGroup,
- groupCollapsed: mockGroupCollapsed,
- };
+ const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
+ rendererID = inject(internals);
- Console.dangerous_setTargetConsoleForTesting(fakeConsole);
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__.dangerous_setTargetConsoleForTesting(
- fakeConsole,
- );
+ return rendererID;
+ };
React = require('react');
if (
@@ -69,137 +42,44 @@ describe('console', () => {
const {render} = getVersionedRenderImplementation();
- // @reactVersion >=18.0
- it('should not patch console methods that are not explicitly overridden', () => {
- expect(fakeConsole.error).not.toBe(mockError);
- expect(fakeConsole.info).toBe(mockInfo);
- expect(fakeConsole.log).toBe(mockLog);
- expect(fakeConsole.warn).not.toBe(mockWarn);
- expect(fakeConsole.group).toBe(mockGroup);
- expect(fakeConsole.groupCollapsed).toBe(mockGroupCollapsed);
- });
-
- // @reactVersion >=18.0
- it('should patch the console when appendComponentStack is enabled', () => {
- unpatchConsole();
-
- expect(fakeConsole.error).toBe(mockError);
- expect(fakeConsole.warn).toBe(mockWarn);
-
- patchConsole({
- appendComponentStack: true,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: false,
- });
-
- expect(fakeConsole.error).not.toBe(mockError);
- expect(fakeConsole.warn).not.toBe(mockWarn);
- });
-
- // @reactVersion >=18.0
- it('should patch the console when breakOnConsoleErrors is enabled', () => {
- unpatchConsole();
-
- expect(fakeConsole.error).toBe(mockError);
- expect(fakeConsole.warn).toBe(mockWarn);
-
- patchConsole({
- appendComponentStack: false,
- breakOnConsoleErrors: true,
- showInlineWarningsAndErrors: false,
- });
-
- expect(fakeConsole.error).not.toBe(mockError);
- expect(fakeConsole.warn).not.toBe(mockWarn);
- });
-
- // @reactVersion >=18.0
- it('should patch the console when showInlineWarningsAndErrors is enabled', () => {
- unpatchConsole();
-
- expect(fakeConsole.error).toBe(mockError);
- expect(fakeConsole.warn).toBe(mockWarn);
-
- patchConsole({
- appendComponentStack: false,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: true,
- });
-
- expect(fakeConsole.error).not.toBe(mockError);
- expect(fakeConsole.warn).not.toBe(mockWarn);
- });
-
- // @reactVersion >=18.0
- it('should only patch the console once', () => {
- const {error, warn} = fakeConsole;
-
- patchConsole({
- appendComponentStack: true,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: false,
- });
-
- expect(fakeConsole.error).toBe(error);
- expect(fakeConsole.warn).toBe(warn);
- });
-
- // @reactVersion >=18.0
- it('should un-patch when requested', () => {
- expect(fakeConsole.error).not.toBe(mockError);
- expect(fakeConsole.warn).not.toBe(mockWarn);
+ // @reactVersion >= 18.0
+ it('should pass through logs when there is no current fiber', () => {
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(0);
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(0);
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(0);
- unpatchConsole();
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
- expect(fakeConsole.error).toBe(mockError);
- expect(fakeConsole.warn).toBe(mockWarn);
+ expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
+ expect(global.consoleWarnMock.mock.calls).toEqual([['warn']]);
+ expect(global.consoleErrorMock.mock.calls).toEqual([['error']]);
});
- // @reactVersion >=18.0
- it('should pass through logs when there is no current fiber', () => {
- expect(mockLog).toHaveBeenCalledTimes(0);
- expect(mockWarn).toHaveBeenCalledTimes(0);
- expect(mockError).toHaveBeenCalledTimes(0);
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
- });
-
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should not append multiple stacks', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
const Child = ({children}) => {
- fakeConsole.warn('warn\n in Child (at fake.js:123)');
- fakeConsole.error('error', '\n in Child (at fake.js:123)');
+ console.warn('warn', '\n in Child (at fake.js:123)');
+ console.error('error', '\n in Child (at fake.js:123)');
return null;
};
act(() => render());
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe(
- 'warn\n in Child (at fake.js:123)',
- );
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)');
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual(['warn', '\n in Child (at **)']);
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual(['error', '\n in Child (at **)']);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should append component stacks to errors and warnings logged during render', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
const Intermediate = ({children}) => children;
const Parent = ({children}) => (
@@ -208,36 +88,34 @@ describe('console', () => {
);
const Child = ({children}) => {
- fakeConsole.error('error');
- fakeConsole.log('log');
- fakeConsole.warn('warn');
+ console.error('error');
+ console.log('log');
+ console.warn('warn');
return null;
};
act(() => render());
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'warn',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'error',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
+ ]);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should append component stacks to errors and warnings logged from effects', () => {
const Intermediate = ({children}) => children;
const Parent = ({children}) => (
@@ -247,60 +125,63 @@ describe('console', () => {
);
const Child = ({children}) => {
React.useLayoutEffect(function Child_useLayoutEffect() {
- fakeConsole.error('active error');
- fakeConsole.log('active log');
- fakeConsole.warn('active warn');
+ console.error('active error');
+ console.log('active log');
+ console.warn('active warn');
});
React.useEffect(function Child_useEffect() {
- fakeConsole.error('passive error');
- fakeConsole.log('passive log');
- fakeConsole.warn('passive warn');
+ console.error('passive error');
+ console.log('passive log');
+ console.warn('passive warn');
});
return null;
};
act(() => render());
- expect(mockLog).toHaveBeenCalledTimes(2);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('active log');
- expect(mockLog.mock.calls[1]).toHaveLength(1);
- expect(mockLog.mock.calls[1][0]).toBe('passive log');
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('active warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(global.consoleLogMock.mock.calls).toEqual([
+ ['active log'],
+ ['passive log'],
+ ]);
+
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'active warn',
supportsOwnerStacks
? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
+ ]);
+ expect(
+ global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'passive warn',
supportsOwnerStacks
? '\n in Child_useEffect (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('active error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
+ ]);
+
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'active error',
supportsOwnerStacks
? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1][0]).toBe('passive error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'passive error',
supportsOwnerStacks
? '\n in Child_useEffect (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
+ ]);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should append component stacks to errors and warnings logged from commit hooks', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
const Intermediate = ({children}) => children;
const Parent = ({children}) => (
@@ -310,14 +191,14 @@ describe('console', () => {
);
class Child extends React.Component {
componentDidMount() {
- fakeConsole.error('didMount error');
- fakeConsole.log('didMount log');
- fakeConsole.warn('didMount warn');
+ console.error('didMount error');
+ console.log('didMount log');
+ console.warn('didMount warn');
}
componentDidUpdate() {
- fakeConsole.error('didUpdate error');
- fakeConsole.log('didUpdate log');
- fakeConsole.warn('didUpdate warn');
+ console.error('didUpdate error');
+ console.log('didUpdate log');
+ console.warn('didUpdate warn');
}
render() {
return null;
@@ -327,44 +208,47 @@ describe('console', () => {
act(() => render());
act(() => render());
- expect(mockLog).toHaveBeenCalledTimes(2);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('didMount log');
- expect(mockLog.mock.calls[1]).toHaveLength(1);
- expect(mockLog.mock.calls[1][0]).toBe('didUpdate log');
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(global.consoleLogMock.mock.calls).toEqual([
+ ['didMount log'],
+ ['didUpdate log'],
+ ]);
+
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'didMount warn',
supportsOwnerStacks
? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
+ ]);
+ expect(
+ global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'didUpdate warn',
supportsOwnerStacks
? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('didMount error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
+ ]);
+
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'didMount error',
supportsOwnerStacks
? '\n in Child.componentDidMount (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'didUpdate error',
supportsOwnerStacks
? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
+ ]);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should append component stacks to errors and warnings logged from gDSFP', () => {
const Intermediate = ({children}) => children;
const Parent = ({children}) => (
@@ -375,9 +259,9 @@ describe('console', () => {
class Child extends React.Component {
state = {};
static getDerivedStateFromProps() {
- fakeConsole.error('error');
- fakeConsole.log('log');
- fakeConsole.warn('warn');
+ console.error('error');
+ console.log('log');
+ console.warn('warn');
return null;
}
render() {
@@ -387,71 +271,27 @@ describe('console', () => {
act(() => render());
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'warn',
supportsOwnerStacks
? '\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'error',
supportsOwnerStacks
? '\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- });
-
- // @reactVersion >=18.0
- it('should append stacks after being uninstalled and reinstalled', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
-
- const Child = ({children}) => {
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- return null;
- };
-
- act(() => render());
-
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
-
- patchConsole({
- appendComponentStack: true,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: false,
- });
- act(() => render());
-
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1][0]).toBe('warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
- '\n in Child (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1][0]).toBe('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
- '\n in Child (at **)',
- );
+ ]);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should be resilient to prepareStackTrace', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
-
Error.prepareStackTrace = function (error, callsites) {
const stack = ['An error occurred:', error.message];
for (let i = 0; i < callsites.length; i++) {
@@ -473,62 +313,66 @@ describe('console', () => {
);
const Child = ({children}) => {
- fakeConsole.error('error');
- fakeConsole.log('log');
- fakeConsole.warn('warn');
+ console.error('error');
+ console.log('log');
+ console.warn('warn');
return null;
};
act(() => render());
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(global.consoleLogMock.mock.calls).toEqual([['log']]);
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'warn',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'error',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
+ ]);
});
- // @reactVersion >=18.0
+ // @reactVersion >= 18.0
it('should correctly log Symbols', () => {
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+
const Component = ({children}) => {
- fakeConsole.warn('Symbol:', Symbol(''));
+ console.warn('Symbol:', Symbol(''));
return null;
};
act(() => render());
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0][0]).toBe('Symbol:');
+ expect(global.consoleWarnMock.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ "Symbol:",
+ Symbol(),
+ ],
+ ]
+ `);
});
it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- fakeConsole.info('info');
- fakeConsole.group('group');
- fakeConsole.groupCollapsed('groupCollapsed');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
return ;
}
@@ -539,77 +383,38 @@ describe('console', () => {
,
),
);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockLog.mock.calls[1]).toEqual([
+
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleLogMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'log',
]);
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1]).toEqual([
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleWarnMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'warn',
]);
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1]).toEqual([
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleErrorMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'error',
]);
-
- expect(mockInfo).toHaveBeenCalledTimes(2);
- expect(mockInfo.mock.calls[0]).toHaveLength(1);
- expect(mockInfo.mock.calls[0][0]).toBe('info');
- expect(mockInfo.mock.calls[1]).toHaveLength(2);
- expect(mockInfo.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'info',
- ]);
-
- expect(mockGroup).toHaveBeenCalledTimes(2);
- expect(mockGroup.mock.calls[0]).toHaveLength(1);
- expect(mockGroup.mock.calls[0][0]).toBe('group');
- expect(mockGroup.mock.calls[1]).toHaveLength(2);
- expect(mockGroup.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'group',
- ]);
-
- expect(mockGroupCollapsed).toHaveBeenCalledTimes(2);
- expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
- expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
- expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2);
- expect(mockGroupCollapsed.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'groupCollapsed',
- ]);
});
it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ true;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
- console.log(
- 'CALL',
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__,
- );
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- fakeConsole.info('info');
- fakeConsole.group('group');
- fakeConsole.groupCollapsed('groupCollapsed');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
return ;
}
@@ -621,54 +426,29 @@ describe('console', () => {
),
);
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
-
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
-
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
-
- expect(mockInfo).toHaveBeenCalledTimes(1);
- expect(mockInfo.mock.calls[0]).toHaveLength(1);
- expect(mockInfo.mock.calls[0][0]).toBe('info');
-
- expect(mockGroup).toHaveBeenCalledTimes(1);
- expect(mockGroup.mock.calls[0]).toHaveLength(1);
- expect(mockGroup.mock.calls[0][0]).toBe('group');
-
- expect(mockGroupCollapsed).toHaveBeenCalledTimes(1);
- expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
- expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
});
it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useEffect(() => {
- fakeConsole.log('log effect create');
- fakeConsole.warn('warn effect create');
- fakeConsole.error('error effect create');
- fakeConsole.info('info effect create');
- fakeConsole.group('group effect create');
- fakeConsole.groupCollapsed('groupCollapsed effect create');
+ console.log('log effect create');
+ console.warn('warn effect create');
+ console.error('error effect create');
return () => {
- fakeConsole.log('log effect cleanup');
- fakeConsole.warn('warn effect cleanup');
- fakeConsole.error('error effect cleanup');
- fakeConsole.info('info effect cleanup');
- fakeConsole.group('group effect cleanup');
- fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
+ console.log('log effect cleanup');
+ console.warn('warn effect cleanup');
+ console.error('error effect cleanup');
};
});
@@ -682,61 +462,41 @@ describe('console', () => {
,
),
);
- expect(mockLog.mock.calls).toEqual([
+ expect(global.consoleLogMock.mock.calls).toEqual([
['log effect create'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect cleanup'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect create'],
]);
- expect(mockWarn.mock.calls).toEqual([
+ expect(global.consoleWarnMock.mock.calls).toEqual([
['warn effect create'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect cleanup'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect create'],
]);
- expect(mockError.mock.calls).toEqual([
+ expect(global.consoleErrorMock.mock.calls).toEqual([
['error effect create'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect cleanup'],
['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect create'],
]);
- expect(mockInfo.mock.calls).toEqual([
- ['info effect create'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect cleanup'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect create'],
- ]);
- expect(mockGroup.mock.calls).toEqual([
- ['group effect create'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect cleanup'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect create'],
- ]);
- expect(mockGroupCollapsed.mock.calls).toEqual([
- ['groupCollapsed effect create'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect cleanup'],
- ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect create'],
- ]);
});
it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ true;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useEffect(() => {
- fakeConsole.log('log effect create');
- fakeConsole.warn('warn effect create');
- fakeConsole.error('error effect create');
- fakeConsole.info('info effect create');
- fakeConsole.group('group effect create');
- fakeConsole.groupCollapsed('groupCollapsed effect create');
+ console.log('log effect create');
+ console.warn('warn effect create');
+ console.error('error effect create');
return () => {
- fakeConsole.log('log effect cleanup');
- fakeConsole.warn('warn effect cleanup');
- fakeConsole.error('error effect cleanup');
- fakeConsole.info('info effect cleanup');
- fakeConsole.group('group effect cleanup');
- fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
+ console.log('log effect cleanup');
+ console.warn('warn effect cleanup');
+ console.error('error effect cleanup');
};
});
@@ -750,31 +510,25 @@ describe('console', () => {
,
),
);
- expect(mockLog.mock.calls).toEqual([['log effect create']]);
- expect(mockWarn.mock.calls).toEqual([['warn effect create']]);
- expect(mockError.mock.calls).toEqual([['error effect create']]);
- expect(mockInfo.mock.calls).toEqual([['info effect create']]);
- expect(mockGroup.mock.calls).toEqual([['group effect create']]);
- expect(mockGroupCollapsed.mock.calls).toEqual([
- ['groupCollapsed effect create'],
- ]);
+
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
});
it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useMemo(() => {
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- fakeConsole.info('info');
- fakeConsole.group('group');
- fakeConsole.groupCollapsed('groupCollapsed');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
}, []);
return ;
}
@@ -786,78 +540,39 @@ describe('console', () => {
,
),
);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockLog.mock.calls[1]).toEqual([
+
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleLogMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'log',
]);
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1]).toEqual([
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleWarnMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'warn',
]);
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1]).toEqual([
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleErrorMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'error',
]);
-
- expect(mockInfo).toHaveBeenCalledTimes(2);
- expect(mockInfo.mock.calls[0]).toHaveLength(1);
- expect(mockInfo.mock.calls[0][0]).toBe('info');
- expect(mockInfo.mock.calls[1]).toHaveLength(2);
- expect(mockInfo.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'info',
- ]);
-
- expect(mockGroup).toHaveBeenCalledTimes(2);
- expect(mockGroup.mock.calls[0]).toHaveLength(1);
- expect(mockGroup.mock.calls[0][0]).toBe('group');
- expect(mockGroup.mock.calls[1]).toHaveLength(2);
- expect(mockGroup.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'group',
- ]);
-
- expect(mockGroupCollapsed).toHaveBeenCalledTimes(2);
- expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
- expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
- expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2);
- expect(mockGroupCollapsed.mock.calls[1]).toEqual([
- '\x1b[2;38;2;124;124;124m%s\x1b[0m',
- 'groupCollapsed',
- ]);
});
it('should not double log from useMemo fns if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ true;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useMemo(() => {
- console.log(
- 'CALL',
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__,
- );
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- fakeConsole.info('info');
- fakeConsole.group('group');
- fakeConsole.groupCollapsed('groupCollapsed');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
}, []);
return ;
}
@@ -870,49 +585,27 @@ describe('console', () => {
),
);
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
-
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
-
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
-
- expect(mockInfo).toHaveBeenCalledTimes(1);
- expect(mockInfo.mock.calls[0]).toHaveLength(1);
- expect(mockInfo.mock.calls[0][0]).toBe('info');
-
- expect(mockGroup).toHaveBeenCalledTimes(1);
- expect(mockGroup.mock.calls[0]).toHaveLength(1);
- expect(mockGroup.mock.calls[0][0]).toBe('group');
-
- expect(mockGroupCollapsed).toHaveBeenCalledTimes(1);
- expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1);
- expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
});
it('should double log in Strict mode initial render for extension', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ false;
// This simulates a render that happens before React DevTools have finished
// their handshake to attach the React DOM renderer functions to DevTools
// In this case, we should still be able to mock the console in Strict mode
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
- rendererID,
- null,
- );
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
return ;
}
@@ -924,52 +617,41 @@ describe('console', () => {
),
);
- expect(mockLog).toHaveBeenCalledTimes(2);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockLog.mock.calls[1]).toHaveLength(2);
- expect(mockLog.mock.calls[1]).toEqual([
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleLogMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'log',
]);
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- expect(mockWarn.mock.calls[1]).toHaveLength(2);
- expect(mockWarn.mock.calls[1]).toEqual([
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleWarnMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'warn',
]);
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(mockError.mock.calls[1]).toHaveLength(2);
- expect(mockError.mock.calls[1]).toEqual([
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(2);
+ expect(global.consoleErrorMock.mock.calls[1]).toEqual([
'\x1b[2;38;2;124;124;124m%s\x1b[0m',
'error',
]);
});
it('should not double log in Strict mode initial render for extension', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ true;
// This simulates a render that happens before React DevTools have finished
// their handshake to attach the React DOM renderer functions to DevTools
// In this case, we should still be able to mock the console in Strict mode
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
- rendererID,
- null,
- );
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID);
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
+ console.log('log');
+ console.warn('warn');
+ console.error('error');
return ;
}
@@ -980,22 +662,16 @@ describe('console', () => {
,
),
);
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(1);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
-
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(1);
- expect(mockError.mock.calls[0][0]).toBe('error');
+ expect(global.consoleLogMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleWarnMock).toHaveBeenCalledTimes(1);
+ expect(global.consoleErrorMock).toHaveBeenCalledTimes(1);
});
it('should properly dim component stacks during strict mode double log', () => {
- global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
- global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true;
+ global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode =
+ false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
@@ -1007,8 +683,8 @@ describe('console', () => {
);
const Child = ({children}) => {
- fakeConsole.error('error');
- fakeConsole.warn('warn');
+ console.error('error');
+ console.warn('warn');
return null;
};
@@ -1020,140 +696,41 @@ describe('console', () => {
),
);
- expect(mockWarn).toHaveBeenCalledTimes(2);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
+ expect(
+ global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'warn',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockWarn.mock.calls[1]).toHaveLength(3);
- expect(mockWarn.mock.calls[1][0]).toEqual(
+ ]);
+
+ expect(
+ global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
- );
- expect(mockWarn.mock.calls[1][1]).toMatch('warn');
- expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual(
+ 'warn',
supportsOwnerStacks
- ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist.
- '\n in Child (at **)\n in Parent (at **)'
+ ? '\n in Child (at **)\n in Parent (at **)'
: 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
+ ]);
- expect(mockError).toHaveBeenCalledTimes(2);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual(
+ expect(
+ global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
+ ).toEqual([
+ 'error',
supportsOwnerStacks
? '\n in Child (at **)\n in Parent (at **)'
: '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- expect(mockError.mock.calls[1]).toHaveLength(3);
- expect(mockError.mock.calls[1][0]).toEqual(
+ ]);
+ expect(
+ global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo),
+ ).toEqual([
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
- );
- expect(mockError.mock.calls[1][1]).toEqual('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual(
+ 'error',
supportsOwnerStacks
- ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist.
- '\n in Child (at **)\n in Parent (at **)'
+ ? '\n in Child (at **)\n in Parent (at **)'
: 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
- );
- });
-});
-
-describe('console error', () => {
- beforeEach(() => {
- jest.resetModules();
-
- const Console = require('react-devtools-shared/src/backend/console');
- patchConsole = Console.patch;
- unpatchConsole = Console.unpatch;
-
- // Patch a fake console so we can verify with tests below.
- // Patching the real console is too complicated,
- // because Jest itself has hooks into it as does our test env setup.
- mockError = jest.fn();
- mockInfo = jest.fn();
- mockGroup = jest.fn();
- mockGroupCollapsed = jest.fn();
- mockLog = jest.fn();
- mockWarn = jest.fn();
- fakeConsole = {
- error: mockError,
- info: mockInfo,
- log: mockLog,
- warn: mockWarn,
- group: mockGroup,
- groupCollapsed: mockGroupCollapsed,
- };
-
- Console.dangerous_setTargetConsoleForTesting(fakeConsole);
-
- const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
- inject(internals);
-
- Console.registerRenderer(
- () => {
- throw Error('foo');
- },
- () => {
- return {
- enableOwnerStacks: true,
- componentStack: '\n at FakeStack (fake-file)',
- };
- },
- );
- };
-
- React = require('react');
- ReactDOMClient = require('react-dom/client');
-
- const utils = require('./utils');
- act = utils.act;
- });
-
- // @reactVersion >=18.0
- it('error in console log throws without interfering with logging', () => {
- const container = document.createElement('div');
- const root = ReactDOMClient.createRoot(container);
-
- function App() {
- fakeConsole.log('log');
- fakeConsole.warn('warn');
- fakeConsole.error('error');
- return ;
- }
-
- patchConsole({
- appendComponentStack: true,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: true,
- hideConsoleLogsInStrictMode: false,
- });
-
- expect(() => {
- act(() => {
- root.render();
- });
- }).toThrowError('foo');
-
- expect(mockLog).toHaveBeenCalledTimes(1);
- expect(mockLog.mock.calls[0]).toHaveLength(1);
- expect(mockLog.mock.calls[0][0]).toBe('log');
-
- expect(mockWarn).toHaveBeenCalledTimes(1);
- expect(mockWarn.mock.calls[0]).toHaveLength(2);
- expect(mockWarn.mock.calls[0][0]).toBe('warn');
- // An error in showInlineWarningsAndErrors doesn't need to break component stacks.
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
- '\n in FakeStack (at **)',
- );
-
- expect(mockError).toHaveBeenCalledTimes(1);
- expect(mockError.mock.calls[0]).toHaveLength(2);
- expect(mockError.mock.calls[0][0]).toBe('error');
- expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
- '\n in FakeStack (at **)',
- );
+ ]);
});
});
diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js
index 79cda67f99b18..d4afd05899a56 100644
--- a/packages/react-devtools-shared/src/__tests__/setupTests.js
+++ b/packages/react-devtools-shared/src/__tests__/setupTests.js
@@ -13,6 +13,7 @@ import type {
BackendBridge,
FrontendBridge,
} from 'react-devtools-shared/src/bridge';
+
const {getTestFlags} = require('../../../../scripts/jest/TestFlags');
// Argument is serialized when passed from jest-cli script through to setupTests.
@@ -103,61 +104,36 @@ global.gate = fn => {
return fn(flags);
};
-beforeEach(() => {
- global.mockClipboardCopy = jest.fn();
-
- // Test environment doesn't support document methods like execCommand()
- // Also once the backend components below have been required,
- // it's too late for a test to mock the clipboard-js modules.
- jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
-
- // These files should be required (and re-required) before each test,
- // rather than imported at the head of the module.
- // That's because we reset modules between tests,
- // which disconnects the DevTool's cache from the current dispatcher ref.
- const Agent = require('react-devtools-shared/src/backend/agent').default;
- const {initBackend} = require('react-devtools-shared/src/backend');
- const Bridge = require('react-devtools-shared/src/bridge').default;
- const Store = require('react-devtools-shared/src/devtools/store').default;
- const {installHook} = require('react-devtools-shared/src/hook');
- const {
- getDefaultComponentFilters,
- setSavedComponentFilters,
- } = require('react-devtools-shared/src/utils');
+function shouldIgnoreConsoleErrorOrWarn(args) {
+ let firstArg = args[0];
+ if (
+ firstArg !== null &&
+ typeof firstArg === 'object' &&
+ String(firstArg).indexOf('Error: Uncaught [') === 0
+ ) {
+ firstArg = String(firstArg);
+ } else if (typeof firstArg !== 'string') {
+ return false;
+ }
- // Fake timers let us flush Bridge operations between setup and assertions.
- jest.useFakeTimers();
+ return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
+ return firstArg.indexOf(errorOrWarningMessage) !== -1;
+ });
+}
- // We use fake timers heavily in tests but the bridge batching now uses microtasks.
- global.devtoolsJestTestScheduler = callback => {
- setTimeout(callback, 0);
- };
+function patchConsoleForTestingBeforeHookInstallation() {
+ const originalConsoleError = console.error;
+ const originalConsoleWarn = console.warn;
+ const originalConsoleLog = console.log;
- // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
- global._ignoredErrorOrWarningMessages = [
- 'react-test-renderer is deprecated.',
- ];
- function shouldIgnoreConsoleErrorOrWarn(args) {
- let firstArg = args[0];
- if (
- firstArg !== null &&
- typeof firstArg === 'object' &&
- String(firstArg).indexOf('Error: Uncaught [') === 0
- ) {
- firstArg = String(firstArg);
- } else if (typeof firstArg !== 'string') {
- return false;
- }
- const shouldFilter = global._ignoredErrorOrWarningMessages.some(
- errorOrWarningMessage => {
- return firstArg.indexOf(errorOrWarningMessage) !== -1;
- },
- );
+ const consoleErrorMock = jest.fn();
+ const consoleWarnMock = jest.fn();
+ const consoleLogMock = jest.fn();
- return shouldFilter;
- }
+ global.consoleErrorMock = consoleErrorMock;
+ global.consoleWarnMock = consoleWarnMock;
+ global.consoleLogMock = consoleLogMock;
- const originalConsoleError = console.error;
console.error = (...args) => {
let firstArg = args[0];
if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) {
@@ -184,17 +160,68 @@ beforeEach(() => {
// Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
return;
}
+
+ consoleErrorMock(...args);
originalConsoleError.apply(console, args);
};
- const originalConsoleWarn = console.warn;
console.warn = (...args) => {
if (shouldIgnoreConsoleErrorOrWarn(args)) {
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
// Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
return;
}
+
+ consoleWarnMock(...args);
originalConsoleWarn.apply(console, args);
};
+ console.log = (...args) => {
+ consoleLogMock(...args);
+ originalConsoleLog.apply(console, args);
+ };
+}
+
+function unpatchConsoleAfterTesting() {
+ delete global.consoleErrorMock;
+ delete global.consoleWarnMock;
+ delete global.consoleLogMock;
+}
+
+beforeEach(() => {
+ patchConsoleForTestingBeforeHookInstallation();
+
+ global.mockClipboardCopy = jest.fn();
+
+ // Test environment doesn't support document methods like execCommand()
+ // Also once the backend components below have been required,
+ // it's too late for a test to mock the clipboard-js modules.
+ jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy}));
+
+ // These files should be required (and re-required) before each test,
+ // rather than imported at the head of the module.
+ // That's because we reset modules between tests,
+ // which disconnects the DevTool's cache from the current dispatcher ref.
+ const Agent = require('react-devtools-shared/src/backend/agent').default;
+ const {initBackend} = require('react-devtools-shared/src/backend');
+ const Bridge = require('react-devtools-shared/src/bridge').default;
+ const Store = require('react-devtools-shared/src/devtools/store').default;
+ const {installHook} = require('react-devtools-shared/src/hook');
+ const {
+ getDefaultComponentFilters,
+ setSavedComponentFilters,
+ } = require('react-devtools-shared/src/utils');
+
+ // Fake timers let us flush Bridge operations between setup and assertions.
+ jest.useFakeTimers();
+
+ // We use fake timers heavily in tests but the bridge batching now uses microtasks.
+ global.devtoolsJestTestScheduler = callback => {
+ setTimeout(callback, 0);
+ };
+
+ // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
+ global._ignoredErrorOrWarningMessages = [
+ 'react-test-renderer is deprecated.',
+ ];
// Initialize filters to a known good state.
setSavedComponentFilters(getDefaultComponentFilters());
@@ -203,7 +230,12 @@ beforeEach(() => {
// Also initialize inline warnings so that we can test them.
global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
- installHook(global);
+ installHook(global, {
+ appendComponentStack: true,
+ breakOnConsoleErrors: false,
+ showInlineWarningsAndErrors: true,
+ hideConsoleLogsInStrictMode: false,
+ });
const bridgeListeners = [];
const bridge = new Bridge({
@@ -221,14 +253,12 @@ beforeEach(() => {
},
});
- const agent = new Agent(((bridge: any): BackendBridge));
+ const store = new Store(((bridge: any): FrontendBridge));
+ const agent = new Agent(((bridge: any): BackendBridge));
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
-
initBackend(hook, agent, global);
- const store = new Store(((bridge: any): FrontendBridge));
-
global.agent = agent;
global.bridge = bridge;
global.store = store;
@@ -243,8 +273,10 @@ beforeEach(() => {
}
global.fetch = mockFetch;
});
+
afterEach(() => {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
+ unpatchConsoleAfterTesting();
// It's important to reset modules between test runs;
// Without this, ReactDOM won't re-inject itself into the new hook.
diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js
index 8112fdcd2584b..814c6c1c40e67 100644
--- a/packages/react-devtools-shared/src/backend/agent.js
+++ b/packages/react-devtools-shared/src/backend/agent.js
@@ -24,7 +24,6 @@ import {
initialize as setupTraceUpdates,
toggleEnabled as setTraceUpdatesEnabled,
} from './views/TraceUpdates';
-import {patch as patchConsole} from './console';
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -36,7 +35,6 @@ import type {
PathMatch,
RendererID,
RendererInterface,
- ConsolePatchSettings,
DevToolsHookSettings,
} from './types';
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
@@ -805,7 +803,7 @@ export default class Agent extends EventEmitter<{
};
updateConsolePatchSettings: (
- settings: $ReadOnly,
+ settings: $ReadOnly,
) => void = settings => {
// Propagate the settings, so Backend can subscribe to it and modify hook
this.emit('updateHookSettings', {
@@ -814,12 +812,6 @@ export default class Agent extends EventEmitter<{
showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
});
-
- // If the frontend preferences have changed,
- // or in the case of React Native- if the backend is just finding out the preferences-
- // then reinstall the console overrides.
- // It's safe to call `patchConsole` multiple times.
- patchConsole(settings);
};
updateComponentFilters: (componentFilters: Array) => void =
diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js
index 0b6b19626a9bb..9e61285b50fe4 100644
--- a/packages/react-devtools-shared/src/backend/console.js
+++ b/packages/react-devtools-shared/src/backend/console.js
@@ -7,416 +7,7 @@
* @flow
*/
-import type {
- ConsolePatchSettings,
- OnErrorOrWarning,
- GetComponentStack,
-} from './types';
-
-import {
- formatConsoleArguments,
- formatWithStyles,
-} from 'react-devtools-shared/src/backend/utils';
-import {
- FIREFOX_CONSOLE_DIMMING_COLOR,
- ANSI_STYLE_DIMMING_TEMPLATE,
- ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
-} from 'react-devtools-shared/src/constants';
-import {castBool} from '../utils';
-
-const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
-
-// React's custom built component stack strings match "\s{4}in"
-// Chrome's prefix matches "\s{4}at"
-const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
-// Firefox and Safari have no prefix ("")
-// but we can fallback to looking for location info (e.g. "foo.js:12:345")
-const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
-
-export function isStringComponentStack(text: string): boolean {
- return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
-}
-
-const STYLE_DIRECTIVE_REGEX = /^%c/;
-
-// This function tells whether or not the arguments for a console
-// method has been overridden by the patchForStrictMode function.
-// If it has we'll need to do some special formatting of the arguments
-// so the console color stays consistent
-function isStrictModeOverride(args: Array): boolean {
- if (__IS_FIREFOX__) {
- return (
- args.length >= 2 &&
- STYLE_DIRECTIVE_REGEX.test(args[0]) &&
- args[1] === FIREFOX_CONSOLE_DIMMING_COLOR
- );
- } else {
- return args.length >= 2 && args[0] === ANSI_STYLE_DIMMING_TEMPLATE;
- }
-}
-
-// We add a suffix to some frames that older versions of React didn't do.
-// To compare if it's equivalent we strip out the suffix to see if they're
-// still equivalent. Similarly, we sometimes use [] and sometimes () so we
-// strip them to for the comparison.
-const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
-function areStackTracesEqual(a: string, b: string): boolean {
- return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
-}
-
-function restorePotentiallyModifiedArgs(args: Array): Array {
- // If the arguments don't have any styles applied, then just copy
- if (!isStrictModeOverride(args)) {
- return args.slice();
- }
-
- if (__IS_FIREFOX__) {
- // Filter out %c from the start of the first argument and color as a second argument
- return [args[0].slice(2)].concat(args.slice(2));
- } else {
- // Filter out the `\x1b...%s\x1b` template
- return args.slice(1);
- }
-}
-
-const injectedRenderers: Array<{
- onErrorOrWarning: ?OnErrorOrWarning,
- getComponentStack: ?GetComponentStack,
-}> = [];
-
-let targetConsole: Object = console;
-let targetConsoleMethods: {[string]: $FlowFixMe} = {};
-for (const method in console) {
- // $FlowFixMe[invalid-computed-prop]
- targetConsoleMethods[method] = console[method];
-}
-
-let unpatchFn: null | (() => void) = null;
-
-// Enables e.g. Jest tests to inject a mock console object.
-export function dangerous_setTargetConsoleForTesting(
- targetConsoleForTesting: Object,
-): void {
- targetConsole = targetConsoleForTesting;
-
- targetConsoleMethods = ({}: {[string]: $FlowFixMe});
- for (const method in targetConsole) {
- // $FlowFixMe[invalid-computed-prop]
- targetConsoleMethods[method] = console[method];
- }
-}
-
-// v16 renderers should use this method to inject internals necessary to generate a component stack.
-// These internals will be used if the console is patched.
-// Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
-export function registerRenderer(
- onErrorOrWarning?: OnErrorOrWarning,
- getComponentStack?: GetComponentStack,
-): void {
- injectedRenderers.push({
- onErrorOrWarning,
- getComponentStack,
- });
-}
-
-const consoleSettingsRef: ConsolePatchSettings = {
- appendComponentStack: false,
- breakOnConsoleErrors: false,
- showInlineWarningsAndErrors: false,
- hideConsoleLogsInStrictMode: false,
-};
-
-// Patches console methods to append component stack for the current fiber.
-// Call unpatch() to remove the injected behavior.
-export function patch({
- appendComponentStack,
- breakOnConsoleErrors,
- showInlineWarningsAndErrors,
- hideConsoleLogsInStrictMode,
-}: $ReadOnly): void {
- // Settings may change after we've patched the console.
- // Using a shared ref allows the patch function to read the latest values.
- consoleSettingsRef.appendComponentStack = appendComponentStack;
- consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
- consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
- consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
-
- if (
- appendComponentStack ||
- breakOnConsoleErrors ||
- showInlineWarningsAndErrors
- ) {
- if (unpatchFn !== null) {
- // Don't patch twice.
- return;
- }
-
- const originalConsoleMethods: {[string]: $FlowFixMe} = {};
-
- unpatchFn = () => {
- for (const method in originalConsoleMethods) {
- try {
- targetConsole[method] = originalConsoleMethods[method];
- } catch (error) {}
- }
- };
-
- OVERRIDE_CONSOLE_METHODS.forEach(method => {
- try {
- const originalMethod = (originalConsoleMethods[method] = targetConsole[
- method
- ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
- ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
- : targetConsole[method]);
-
- // $FlowFixMe[missing-local-annot]
- const overrideMethod = (...args) => {
- let alreadyHasComponentStack = false;
- if (method !== 'log' && consoleSettingsRef.appendComponentStack) {
- const lastArg = args.length > 0 ? args[args.length - 1] : null;
- alreadyHasComponentStack =
- typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
- }
-
- const shouldShowInlineWarningsAndErrors =
- consoleSettingsRef.showInlineWarningsAndErrors &&
- (method === 'error' || method === 'warn');
-
- // Search for the first renderer that has a current Fiber.
- // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
- for (let i = 0; i < injectedRenderers.length; i++) {
- const renderer = injectedRenderers[i];
- const {getComponentStack, onErrorOrWarning} = renderer;
- try {
- if (shouldShowInlineWarningsAndErrors) {
- // patch() is called by two places: (1) the hook and (2) the renderer backend.
- // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
- if (onErrorOrWarning != null) {
- onErrorOrWarning(
- ((method: any): 'error' | 'warn'),
- // Restore and copy args before we mutate them (e.g. adding the component stack)
- restorePotentiallyModifiedArgs(args),
- );
- }
- }
- } catch (error) {
- // Don't let a DevTools or React internal error interfere with logging.
- setTimeout(() => {
- throw error;
- }, 0);
- }
- try {
- if (
- consoleSettingsRef.appendComponentStack &&
- getComponentStack != null
- ) {
- // This needs to be directly in the wrapper so we can pop exactly one frame.
- const topFrame = Error('react-stack-top-frame');
- const match = getComponentStack(topFrame);
- if (match !== null) {
- const {enableOwnerStacks, componentStack} = match;
- // Empty string means we have a match but no component stack.
- // We don't need to look in other renderers but we also don't add anything.
- if (componentStack !== '') {
- // Create a fake Error so that when we print it we get native source maps. Every
- // browser will print the .stack property of the error and then parse it back for source
- // mapping. Rather than print the internal slot. So it doesn't matter that the internal
- // slot doesn't line up.
- const fakeError = new Error('');
- // In Chromium, only the stack property is printed but in Firefox the :
- // gets printed so to make the colon make sense, we name it so we print Stack:
- // and similarly Safari leave an expandable slot.
- if (__IS_CHROME__ || __IS_EDGE__) {
- // Before sending the stack to Chrome DevTools for formatting,
- // V8 will reconstruct this according to the template :
- // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
- // It has to start with ^[\w.]*Error\b to trigger stack formatting.
- fakeError.name = enableOwnerStacks
- ? 'Error Stack'
- : 'Error Component Stack'; // This gets printed
- } else {
- fakeError.name = enableOwnerStacks
- ? 'Stack'
- : 'Component Stack'; // This gets printed
- }
- // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
- // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
- // to our own stack.
- fakeError.stack =
- __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
- ? (enableOwnerStacks
- ? 'Error Stack:'
- : 'Error Component Stack:') + componentStack
- : componentStack;
-
- if (alreadyHasComponentStack) {
- // Only modify the component stack if it matches what we would've added anyway.
- // Otherwise we assume it was a non-React stack.
- if (isStrictModeOverride(args)) {
- // We do nothing to Strict Mode overrides that already has a stack
- // because we have already lost some context for how to format it
- // since we've already merged the stack into the log at this point.
- } else if (
- areStackTracesEqual(
- args[args.length - 1],
- componentStack,
- )
- ) {
- const firstArg = args[0];
- if (
- args.length > 1 &&
- typeof firstArg === 'string' &&
- firstArg.endsWith('%s')
- ) {
- args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
- }
- args[args.length - 1] = fakeError;
- }
- } else {
- args.push(fakeError);
- if (isStrictModeOverride(args)) {
- if (__IS_FIREFOX__) {
- args[0] = `${args[0]} %o`;
- } else {
- args[0] =
- ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
- }
- }
- }
- }
- // Don't add stacks from other renderers.
- break;
- }
- }
- } catch (error) {
- // Don't let a DevTools or React internal error interfere with logging.
- setTimeout(() => {
- throw error;
- }, 0);
- }
- }
-
- if (consoleSettingsRef.breakOnConsoleErrors) {
- // --- Welcome to debugging with React DevTools ---
- // This debugger statement means that you've enabled the "break on warnings" feature.
- // Use the browser's Call Stack panel to step out of this override function-
- // to where the original warning or error was logged.
- // eslint-disable-next-line no-debugger
- debugger;
- }
-
- originalMethod(...args);
- };
-
- overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
- originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
-
- targetConsole[method] = overrideMethod;
- } catch (error) {}
- });
- } else {
- unpatch();
- }
-}
-
-// Removed component stack patch from console methods.
-export function unpatch(): void {
- if (unpatchFn !== null) {
- unpatchFn();
- unpatchFn = null;
- }
-}
-
-let unpatchForStrictModeFn: null | (() => void) = null;
-
-// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode
-export function patchForStrictMode() {
- const overrideConsoleMethods = [
- 'error',
- 'group',
- 'groupCollapsed',
- 'info',
- 'log',
- 'trace',
- 'warn',
- ];
-
- if (unpatchForStrictModeFn !== null) {
- // Don't patch twice.
- return;
- }
-
- const originalConsoleMethods: {[string]: $FlowFixMe} = {};
-
- unpatchForStrictModeFn = () => {
- for (const method in originalConsoleMethods) {
- try {
- targetConsole[method] = originalConsoleMethods[method];
- } catch (error) {}
- }
- };
-
- overrideConsoleMethods.forEach(method => {
- try {
- const originalMethod = (originalConsoleMethods[method] = targetConsole[
- method
- ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
- ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
- : targetConsole[method]);
-
- // $FlowFixMe[missing-local-annot]
- const overrideMethod = (...args) => {
- if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
- // Dim the text color of the double logs if we're not hiding them.
- if (__IS_FIREFOX__) {
- originalMethod(
- ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
- );
- } else {
- originalMethod(
- ANSI_STYLE_DIMMING_TEMPLATE,
- ...formatConsoleArguments(...args),
- );
- }
- }
- };
-
- overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
- originalMethod;
- originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
- overrideMethod;
-
- targetConsole[method] = overrideMethod;
- } catch (error) {}
- });
-}
-
-// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode
-export function unpatchForStrictMode(): void {
- if (unpatchForStrictModeFn !== null) {
- unpatchForStrictModeFn();
- unpatchForStrictModeFn = null;
- }
-}
-
-export function patchConsoleUsingWindowValues() {
- const appendComponentStack =
- castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true;
- const breakOnConsoleErrors =
- castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false;
- const showInlineWarningsAndErrors =
- castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ?? true;
- const hideConsoleLogsInStrictMode =
- castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ??
- false;
-
- patch({
- appendComponentStack,
- breakOnConsoleErrors,
- showInlineWarningsAndErrors,
- hideConsoleLogsInStrictMode,
- });
-}
+import type {ConsolePatchSettings} from './types';
// After receiving cached console patch settings from React Native, we set them on window.
// When the console is initially patched (in renderer.js and hook.js), these values are read.
@@ -433,10 +24,3 @@ export function writeConsolePatchSettingsToWindow(
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
settings.hideConsoleLogsInStrictMode;
}
-
-export function installConsoleFunctionsToWindow(): void {
- window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
- patchConsoleUsingWindowValues,
- registerRendererWithConsole: registerRenderer,
- };
-}
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index 2fd768b5d01c3..b8636c557ea25 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -71,12 +71,6 @@ import {
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
} from '../../constants';
import {inspectHooksOfFiber} from 'react-debug-tools';
-import {
- patchConsoleUsingWindowValues,
- registerRenderer as registerRendererWithConsole,
- patchForStrictMode as patchConsoleForStrictMode,
- unpatchForStrictMode as unpatchConsoleForStrictMode,
-} from '../console';
import {
CONCURRENT_MODE_NUMBER,
CONCURRENT_MODE_SYMBOL_STRING,
@@ -1198,16 +1192,6 @@ export function attach(
needsToFlushComponentLogs = true;
}
- // Patching the console enables DevTools to do a few useful things:
- // * Append component stacks to warnings and error messages
- // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
- registerRendererWithConsole(onErrorOrWarning, getComponentStack);
-
- // The renderer interface can't read these preferences directly,
- // because it is stored in localStorage within the context of the extension.
- // It relies on the extension to pass the preference through via the global.
- patchConsoleUsingWindowValues();
-
function debug(
name: string,
instance: DevToolsInstance,
@@ -5788,7 +5772,6 @@ export function attach(
hasElementWithId,
inspectElement,
logElementToConsole,
- patchConsoleForStrictMode,
getComponentStack,
getElementAttributeByPath,
getElementSourceFunctionById,
@@ -5803,7 +5786,6 @@ export function attach(
startProfiling,
stopProfiling,
storeAsGlobal,
- unpatchConsoleForStrictMode,
updateComponentFilters,
getEnvironmentNames,
};
diff --git a/packages/react-devtools-shared/src/backend/flight/renderer.js b/packages/react-devtools-shared/src/backend/flight/renderer.js
index 065dc81a071a7..3d8befd4215e7 100644
--- a/packages/react-devtools-shared/src/backend/flight/renderer.js
+++ b/packages/react-devtools-shared/src/backend/flight/renderer.js
@@ -19,11 +19,6 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils';
-import {
- patchConsoleUsingWindowValues,
- registerRenderer as registerRendererWithConsole,
-} from '../console';
-
function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
// If this ReactComponentInfo supports native console.createTask then we are already running
// inside a native async stack trace if it's active - meaning the DevTools is open.
@@ -145,9 +140,6 @@ export function attach(
// The changes will be flushed later when we commit this tree to Fiber.
}
- patchConsoleUsingWindowValues();
- registerRendererWithConsole(onErrorOrWarning, getComponentStack);
-
return {
cleanup() {},
clearErrorsAndWarnings() {},
@@ -205,7 +197,6 @@ export function attach(
};
},
logElementToConsole() {},
- patchConsoleForStrictMode() {},
getElementAttributeByPath() {},
getElementSourceFunctionById() {},
onErrorOrWarning,
@@ -219,7 +210,6 @@ export function attach(
startProfiling() {},
stopProfiling() {},
storeAsGlobal() {},
- unpatchConsoleForStrictMode() {},
updateComponentFilters() {},
getEnvironmentNames() {
return [];
diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js
index 828d05fc27f81..8e26dcae445ee 100644
--- a/packages/react-devtools-shared/src/backend/legacy/renderer.js
+++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js
@@ -1103,10 +1103,6 @@ export function attach(
// Not implemented
}
- function patchConsoleForStrictMode() {}
-
- function unpatchConsoleForStrictMode() {}
-
function hasElementWithId(id: number): boolean {
return idToInternalInstanceMap.has(id);
}
@@ -1141,7 +1137,6 @@ export function attach(
overrideSuspense,
overrideValueAtPath,
renamePath,
- patchConsoleForStrictMode,
getElementAttributeByPath,
getElementSourceFunctionById,
renderer,
@@ -1150,7 +1145,6 @@ export function attach(
startProfiling,
stopProfiling,
storeAsGlobal,
- unpatchConsoleForStrictMode,
updateComponentFilters,
getEnvironmentNames,
};
diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js
index fa9949e3ddc5d..bdc93a19baa8b 100644
--- a/packages/react-devtools-shared/src/backend/types.js
+++ b/packages/react-devtools-shared/src/backend/types.js
@@ -404,7 +404,6 @@ export type RendererInterface = {
path: Array,
value: any,
) => void,
- patchConsoleForStrictMode: () => void,
getElementAttributeByPath: (
id: number,
path: Array,
@@ -427,7 +426,6 @@ export type RendererInterface = {
path: Array,
count: number,
) => void,
- unpatchConsoleForStrictMode: () => void,
updateComponentFilters: (componentFilters: Array) => void,
getEnvironmentNames: () => Array,
diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js
index 65a52b571a680..156319b366d71 100644
--- a/packages/react-devtools-shared/src/bridge.js
+++ b/packages/react-devtools-shared/src/bridge.js
@@ -15,7 +15,7 @@ import type {
OwnersList,
ProfilingDataBackend,
RendererID,
- ConsolePatchSettings,
+ DevToolsHookSettings,
} from 'react-devtools-shared/src/backend/types';
import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
@@ -241,7 +241,7 @@ type FrontendEvents = {
storeAsGlobal: [StoreAsGlobalParams],
updateComponentFilters: [Array],
getEnvironmentNames: [],
- updateConsolePatchSettings: [ConsolePatchSettings],
+ updateConsolePatchSettings: [DevToolsHookSettings],
viewAttributeSource: [ViewAttributeSourceParams],
viewElementSource: [ElementAndRendererID],
diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js
index 1dd1fcbd4c455..ac56d13d6ab4e 100644
--- a/packages/react-devtools-shared/src/hook.js
+++ b/packages/react-devtools-shared/src/hook.js
@@ -21,10 +21,31 @@ import type {
import {
FIREFOX_CONSOLE_DIMMING_COLOR,
ANSI_STYLE_DIMMING_TEMPLATE,
+ ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
} from 'react-devtools-shared/src/constants';
import attachRenderer from './attachRenderer';
-declare var window: any;
+// React's custom built component stack strings match "\s{4}in"
+// Chrome's prefix matches "\s{4}at"
+const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
+// Firefox and Safari have no prefix ("")
+// but we can fallback to looking for location info (e.g. "foo.js:12:345")
+const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
+
+function isStringComponentStack(text: string): boolean {
+ return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
+}
+
+// We add a suffix to some frames that older versions of React didn't do.
+// To compare if it's equivalent we strip out the suffix to see if they're
+// still equivalent. Similarly, we sometimes use [] and sometimes () so we
+// strip them to for the comparison.
+const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
+function areStackTracesEqual(a: string, b: string): boolean {
+ return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
+}
+
+const targetConsole: Object = console;
export function installHook(
target: any,
@@ -36,25 +57,6 @@ export function installHook(
return null;
}
- let targetConsole: Object = console;
- let targetConsoleMethods: {[string]: $FlowFixMe} = {};
- for (const method in console) {
- // $FlowFixMe[invalid-computed-prop]
- targetConsoleMethods[method] = console[method];
- }
-
- function dangerous_setTargetConsoleForTesting(
- targetConsoleForTesting: Object,
- ): void {
- targetConsole = targetConsoleForTesting;
-
- targetConsoleMethods = ({}: {[string]: $FlowFixMe});
- for (const method in targetConsole) {
- // $FlowFixMe[invalid-computed-prop]
- targetConsoleMethods[method] = console[method];
- }
- }
-
function detectReactBuildType(renderer: ReactRenderer) {
try {
if (typeof renderer.version === 'string') {
@@ -189,10 +191,7 @@ export function installHook(
}
// NOTE: KEEP IN SYNC with src/backend/utils.js
- function formatWithStyles(
- inputArgs: $ReadOnlyArray,
- style?: string,
- ): $ReadOnlyArray {
+ function formatWithStyles(inputArgs: Array, style?: string): Array {
if (
inputArgs === undefined ||
inputArgs === null ||
@@ -285,85 +284,6 @@ export function installHook(
return [template, ...args];
}
- let unpatchFn = null;
-
- // NOTE: KEEP IN SYNC with src/backend/console.js:patchForStrictMode
- // This function hides or dims console logs during the initial double renderer
- // in Strict Mode. We need this function because during initial render,
- // React and DevTools are connecting and the renderer interface isn't avaiable
- // and we want to be able to have consistent logging behavior for double logs
- // during the initial renderer.
- function patchConsoleForInitialCommitInStrictMode(
- hideConsoleLogsInStrictMode: boolean,
- ) {
- const overrideConsoleMethods = [
- 'error',
- 'group',
- 'groupCollapsed',
- 'info',
- 'log',
- 'trace',
- 'warn',
- ];
-
- if (unpatchFn !== null) {
- // Don't patch twice.
- return;
- }
-
- const originalConsoleMethods: {[string]: $FlowFixMe} = {};
-
- unpatchFn = () => {
- for (const method in originalConsoleMethods) {
- try {
- targetConsole[method] = originalConsoleMethods[method];
- } catch (error) {}
- }
- };
-
- overrideConsoleMethods.forEach(method => {
- try {
- const originalMethod = (originalConsoleMethods[method] = targetConsole[
- method
- ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
- ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
- : targetConsole[method]);
-
- const overrideMethod = (...args: $ReadOnlyArray) => {
- // Dim the text color of the double logs if we're not hiding them.
- if (!hideConsoleLogsInStrictMode) {
- // Firefox doesn't support ANSI escape sequences
- if (__IS_FIREFOX__) {
- originalMethod(
- ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
- );
- } else {
- originalMethod(
- ANSI_STYLE_DIMMING_TEMPLATE,
- ...formatConsoleArguments(...args),
- );
- }
- }
- };
-
- overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
- originalMethod;
- originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
- overrideMethod;
-
- targetConsole[method] = overrideMethod;
- } catch (error) {}
- });
- }
-
- // NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
- function unpatchConsoleForInitialCommitInStrictMode() {
- if (unpatchFn !== null) {
- unpatchFn();
- unpatchFn = null;
- }
- }
-
let uidCounter = 0;
function inject(renderer: ReactRenderer): number {
const id = ++uidCounter;
@@ -469,28 +389,85 @@ export function installHook(
}
}
- function setStrictMode(rendererID: RendererID, isStrictMode: any) {
- const rendererInterface = rendererInterfaces.get(rendererID);
- if (rendererInterface != null) {
- if (isStrictMode) {
- rendererInterface.patchConsoleForStrictMode();
- } else {
- rendererInterface.unpatchConsoleForStrictMode();
- }
+ let isRunningDuringStrictModeInvocation = false;
+ function setStrictMode(rendererID: RendererID, isStrictMode: boolean) {
+ isRunningDuringStrictModeInvocation = isStrictMode;
+
+ if (isStrictMode) {
+ patchConsoleForStrictMode();
} else {
- // This should only happen during initial commit in the extension before DevTools
- // finishes its handshake with the injected renderer
- if (isStrictMode) {
- const hideConsoleLogsInStrictMode =
- window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
-
- patchConsoleForInitialCommitInStrictMode(hideConsoleLogsInStrictMode);
- } else {
- unpatchConsoleForInitialCommitInStrictMode();
- }
+ unpatchConsoleForStrictMode();
+ }
+ }
+
+ const unpatchConsoleCallbacks = [];
+ // For StrictMode we patch console once we are running in StrictMode and unpatch right after it
+ // So patching could happen multiple times during the runtime
+ // Notice how we don't patch error or warn methods, because they are already patched in patchConsoleForErrorsAndWarnings
+ // This will only happen once, when hook is installed
+ function patchConsoleForStrictMode() {
+ // Don't patch console in case settings were not injected
+ if (!hook.settings) {
+ return;
+ }
+
+ // Don't patch twice
+ if (unpatchConsoleCallbacks.length > 0) {
+ return;
+ }
+
+ // At this point 'error', 'warn', and 'trace' methods are already patched
+ // by React DevTools hook to append component stacks and other possible features.
+ const consoleMethodsToOverrideForStrictMode = [
+ 'group',
+ 'groupCollapsed',
+ 'info',
+ 'log',
+ ];
+
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const method of consoleMethodsToOverrideForStrictMode) {
+ const originalMethod = targetConsole[method];
+ const overrideMethod: (...args: Array) => void = (
+ ...args: any[]
+ ) => {
+ const settings = hook.settings;
+ // Something unexpected happened, fallback to just printing the console message.
+ if (settings == null) {
+ originalMethod(...args);
+ return;
+ }
+
+ if (settings.hideConsoleLogsInStrictMode) {
+ return;
+ }
+
+ // Dim the text color of the double logs if we're not hiding them.
+ // Firefox doesn't support ANSI escape sequences
+ if (__IS_FIREFOX__) {
+ originalMethod(
+ ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
+ );
+ } else {
+ originalMethod(
+ ANSI_STYLE_DIMMING_TEMPLATE,
+ ...formatConsoleArguments(...args),
+ );
+ }
+ };
+
+ targetConsole[method] = overrideMethod;
+ unpatchConsoleCallbacks.push(() => {
+ targetConsole[method] = originalMethod;
+ });
}
}
+ function unpatchConsoleForStrictMode() {
+ unpatchConsoleCallbacks.forEach(callback => callback());
+ unpatchConsoleCallbacks.length = 0;
+ }
+
type StackFrameString = string;
const openModuleRangesStack: Array = [];
@@ -526,6 +503,188 @@ export function installHook(
}
}
+ // For Errors and Warnings we only patch console once
+ function patchConsoleForErrorsAndWarnings() {
+ // Don't patch console in case settings were not injected
+ if (!hook.settings) {
+ return;
+ }
+
+ const consoleMethodsToOverrideForErrorsAndWarnings = [
+ 'error',
+ 'trace',
+ 'warn',
+ ];
+
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const method of consoleMethodsToOverrideForErrorsAndWarnings) {
+ const originalMethod = targetConsole[method];
+ const overrideMethod: (...args: Array) => void = (...args) => {
+ const settings = hook.settings;
+ // Something unexpected happened, fallback to just printing the console message.
+ if (settings == null) {
+ originalMethod(...args);
+ return;
+ }
+
+ if (
+ isRunningDuringStrictModeInvocation &&
+ settings.hideConsoleLogsInStrictMode
+ ) {
+ return;
+ }
+
+ let injectedComponentStackAsFakeError = false;
+ let alreadyHasComponentStack = false;
+ if (settings.appendComponentStack) {
+ const lastArg = args.length > 0 ? args[args.length - 1] : null;
+ alreadyHasComponentStack =
+ typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
+ }
+
+ const shouldShowInlineWarningsAndErrors =
+ settings.showInlineWarningsAndErrors &&
+ (method === 'error' || method === 'warn');
+
+ // Search for the first renderer that has a current Fiber.
+ // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
+ for (const rendererInterface of hook.rendererInterfaces.values()) {
+ const {onErrorOrWarning, getComponentStack} = rendererInterface;
+ try {
+ if (shouldShowInlineWarningsAndErrors) {
+ // patch() is called by two places: (1) the hook and (2) the renderer backend.
+ // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
+ if (onErrorOrWarning != null) {
+ onErrorOrWarning(
+ ((method: any): 'error' | 'warn'),
+ args.slice(),
+ );
+ }
+ }
+ } catch (error) {
+ // Don't let a DevTools or React internal error interfere with logging.
+ setTimeout(() => {
+ throw error;
+ }, 0);
+ }
+
+ try {
+ if (settings.appendComponentStack && getComponentStack != null) {
+ // This needs to be directly in the wrapper so we can pop exactly one frame.
+ const topFrame = Error('react-stack-top-frame');
+ const match = getComponentStack(topFrame);
+ if (match !== null) {
+ const {enableOwnerStacks, componentStack} = match;
+ // Empty string means we have a match but no component stack.
+ // We don't need to look in other renderers but we also don't add anything.
+ if (componentStack !== '') {
+ // Create a fake Error so that when we print it we get native source maps. Every
+ // browser will print the .stack property of the error and then parse it back for source
+ // mapping. Rather than print the internal slot. So it doesn't matter that the internal
+ // slot doesn't line up.
+ const fakeError = new Error('');
+ // In Chromium, only the stack property is printed but in Firefox the :
+ // gets printed so to make the colon make sense, we name it so we print Stack:
+ // and similarly Safari leave an expandable slot.
+ if (__IS_CHROME__ || __IS_EDGE__) {
+ // Before sending the stack to Chrome DevTools for formatting,
+ // V8 will reconstruct this according to the template :
+ // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
+ // It has to start with ^[\w.]*Error\b to trigger stack formatting.
+ fakeError.name = enableOwnerStacks
+ ? 'Error Stack'
+ : 'Error Component Stack'; // This gets printed
+ } else {
+ fakeError.name = enableOwnerStacks
+ ? 'Stack'
+ : 'Component Stack'; // This gets printed
+ }
+ // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
+ // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
+ // to our own stack.
+ fakeError.stack =
+ __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
+ ? (enableOwnerStacks
+ ? 'Error Stack:'
+ : 'Error Component Stack:') + componentStack
+ : componentStack;
+
+ if (alreadyHasComponentStack) {
+ // Only modify the component stack if it matches what we would've added anyway.
+ // Otherwise we assume it was a non-React stack.
+ if (
+ areStackTracesEqual(args[args.length - 1], componentStack)
+ ) {
+ const firstArg = args[0];
+ if (
+ args.length > 1 &&
+ typeof firstArg === 'string' &&
+ firstArg.endsWith('%s')
+ ) {
+ args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
+ }
+ args[args.length - 1] = fakeError;
+ injectedComponentStackAsFakeError = true;
+ }
+ } else {
+ args.push(fakeError);
+ injectedComponentStackAsFakeError = true;
+ }
+ }
+
+ // Don't add stacks from other renderers.
+ break;
+ }
+ }
+ } catch (error) {
+ // Don't let a DevTools or React internal error interfere with logging.
+ setTimeout(() => {
+ throw error;
+ }, 0);
+ }
+ }
+
+ if (settings.breakOnConsoleErrors) {
+ // --- Welcome to debugging with React DevTools ---
+ // This debugger statement means that you've enabled the "break on warnings" feature.
+ // Use the browser's Call Stack panel to step out of this override function
+ // to where the original warning or error was logged.
+ // eslint-disable-next-line no-debugger
+ debugger;
+ }
+
+ if (isRunningDuringStrictModeInvocation) {
+ // Dim the text color of the double logs if we're not hiding them.
+ // Firefox doesn't support ANSI escape sequences
+ if (__IS_FIREFOX__) {
+ const argsWithCSSStyles = formatWithStyles(
+ args,
+ FIREFOX_CONSOLE_DIMMING_COLOR,
+ );
+
+ if (injectedComponentStackAsFakeError) {
+ argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`;
+ }
+
+ originalMethod(...argsWithCSSStyles);
+ } else {
+ originalMethod(
+ injectedComponentStackAsFakeError
+ ? ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK
+ : ANSI_STYLE_DIMMING_TEMPLATE,
+ ...formatConsoleArguments(...args),
+ );
+ }
+ } else {
+ originalMethod(...args);
+ }
+ };
+
+ targetConsole[method] = overrideMethod;
+ }
+ }
+
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
const fiberRoots: {[RendererID]: Set} = {};
const rendererInterfaces = new Map();
@@ -580,10 +739,12 @@ export function installHook(
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false,
};
+ patchConsoleForErrorsAndWarnings();
} else {
Promise.resolve(maybeSettingsOrSettingsPromise)
.then(settings => {
hook.settings = settings;
+ patchConsoleForErrorsAndWarnings();
})
.catch(() => {
targetConsole.error(
@@ -592,11 +753,6 @@ export function installHook(
});
}
- if (__TEST__) {
- hook.dangerous_setTargetConsoleForTesting =
- dangerous_setTargetConsoleForTesting;
- }
-
Object.defineProperty(
target,
'__REACT_DEVTOOLS_GLOBAL_HOOK__',