mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Expose DEV-mode warnings in devtools UI (#20463)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
This commit is contained in:
committed by
GitHub
parent
6cbb9394d1
commit
09a2c363a5
@@ -20,6 +20,7 @@ import {
|
||||
getAppendComponentStack,
|
||||
getBreakOnConsoleErrors,
|
||||
getSavedComponentFilters,
|
||||
getShowInlineWarningsAndErrors,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {Server} from 'ws';
|
||||
import {join} from 'path';
|
||||
@@ -303,6 +304,9 @@ function startServer(
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
||||
getSavedComponentFilters(),
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
|
||||
getShowInlineWarningsAndErrors(),
|
||||
)};`;
|
||||
|
||||
response.end(
|
||||
|
||||
+7
-6
@@ -10,6 +10,7 @@ import {
|
||||
getAppendComponentStack,
|
||||
getBreakOnConsoleErrors,
|
||||
getSavedComponentFilters,
|
||||
getShowInlineWarningsAndErrors,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
localStorageGetItem,
|
||||
@@ -29,18 +30,18 @@ let panelCreated = false;
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass filters through.
|
||||
function syncSavedPreferences() {
|
||||
const appendComponentStack = getAppendComponentStack();
|
||||
const breakOnConsoleErrors = getBreakOnConsoleErrors();
|
||||
const componentFilters = getSavedComponentFilters();
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
|
||||
appendComponentStack,
|
||||
getAppendComponentStack(),
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
|
||||
breakOnConsoleErrors,
|
||||
getBreakOnConsoleErrors(),
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
||||
componentFilters,
|
||||
getSavedComponentFilters(),
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
|
||||
getShowInlineWarningsAndErrors(),
|
||||
)};`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ function startActivation(contentWindow: window) {
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
componentFilters,
|
||||
showInlineWarningsAndErrors,
|
||||
} = data;
|
||||
|
||||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
|
||||
// TRICKY
|
||||
// The backend entry point may be required in the context of an iframe or the parent window.
|
||||
@@ -40,6 +42,7 @@ function startActivation(contentWindow: window) {
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
}
|
||||
|
||||
finishActivation(contentWindow);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getAppendComponentStack,
|
||||
getBreakOnConsoleErrors,
|
||||
getSavedComponentFilters,
|
||||
getShowInlineWarningsAndErrors,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
|
||||
@@ -41,6 +42,7 @@ export function initialize(
|
||||
appendComponentStack: getAppendComponentStack(),
|
||||
breakOnConsoleErrors: getBreakOnConsoleErrors(),
|
||||
componentFilters: getSavedComponentFilters(),
|
||||
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
|
||||
},
|
||||
'*',
|
||||
);
|
||||
|
||||
+4
-4
@@ -12,19 +12,19 @@ Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"hocDisplayNames": null,
|
||||
"id": 7,
|
||||
"id": 2,
|
||||
"type": 5,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"hocDisplayNames": null,
|
||||
"id": 9,
|
||||
"id": 3,
|
||||
"type": 5,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Child",
|
||||
"hocDisplayNames": null,
|
||||
"id": 8,
|
||||
"id": 4,
|
||||
"type": 5,
|
||||
},
|
||||
]
|
||||
@@ -115,7 +115,7 @@ Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"hocDisplayNames": null,
|
||||
"id": 5,
|
||||
"id": 2,
|
||||
"type": 5,
|
||||
},
|
||||
]
|
||||
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Store component filters should filter HOCs: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Component> [Bar][Foo]
|
||||
▾ <Component> [Foo]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter HOCs: 2: hide all HOCs 1`] = `
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter HOCs: 3: disable HOC filter 1`] = `
|
||||
[root]
|
||||
▾ <Component> [Bar][Foo]
|
||||
▾ <Component> [Foo]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 2: filter "Foo" 1`] = `
|
||||
[root]
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 3: filter "Ba" 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
<Text>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 4: filter "B.z" 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by path: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by path: 2: hide all components declared within this test filed 1`] = `[root]`;
|
||||
|
||||
exports[`Store component filters should filter by path: 3: hide components in a made up fake path 1`] = `
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should ignore invalid ElementTypeRoot filter: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should ignore invalid ElementTypeRoot filter: 2: add invalid filter 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 1: suspended 1`] = `
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
▾ <Loading>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 2: resolved 1`] = `
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
<Component>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should not break when Suspense nodes are filtered from the tree: 3: suspended 1`] = `
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
▾ <Loading>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 2: hide host components 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<Component>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 3: hide class components 1`] = `
|
||||
[root]
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 4: hide class and function components 1`] = `
|
||||
[root]
|
||||
▾ <div>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 5: disable all filters 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
+442
-21
@@ -15,6 +15,7 @@ import type {
|
||||
} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type Store from 'react-devtools-shared/src/devtools/store';
|
||||
import {withErrorsOrWarningsIgnored} from 'react-devtools-shared/src/__tests__/utils';
|
||||
|
||||
describe('InspectedElementContext', () => {
|
||||
let React;
|
||||
@@ -66,6 +67,10 @@ describe('InspectedElementContext', () => {
|
||||
.TreeContextController;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const Contexts = ({
|
||||
children,
|
||||
defaultSelectedElementID = null,
|
||||
@@ -386,10 +391,10 @@ describe('InspectedElementContext', () => {
|
||||
it('should temporarily disable console logging when re-running a component to inspect its hooks', async done => {
|
||||
let targetRenderCount = 0;
|
||||
|
||||
const errorSpy = ((console: any).error = jest.fn());
|
||||
const infoSpy = ((console: any).info = jest.fn());
|
||||
const logSpy = ((console: any).log = jest.fn());
|
||||
const warnSpy = ((console: any).warn = jest.fn());
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'info').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const Target = React.memo(props => {
|
||||
targetRenderCount++;
|
||||
@@ -407,14 +412,14 @@ describe('InspectedElementContext', () => {
|
||||
);
|
||||
|
||||
expect(targetRenderCount).toBe(1);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith('error');
|
||||
expect(infoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(infoSpy).toHaveBeenCalledWith('info');
|
||||
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledWith('log');
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith('warn');
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledWith('error');
|
||||
expect(console.info).toHaveBeenCalledTimes(1);
|
||||
expect(console.info).toHaveBeenCalledWith('info');
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(console.log).toHaveBeenCalledWith('log');
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith('warn');
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
@@ -442,10 +447,10 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
expect(inspectedElement).not.toBe(null);
|
||||
expect(targetRenderCount).toBe(2);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(infoSpy).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).toHaveBeenCalledTimes(1);
|
||||
expect(console.info).toHaveBeenCalledTimes(1);
|
||||
expect(console.log).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -1569,21 +1574,20 @@ describe('InspectedElementContext', () => {
|
||||
);
|
||||
expect(storeAsGlobal).not.toBeNull();
|
||||
|
||||
const logSpy = jest.fn();
|
||||
spyOn(console, 'log').and.callFake(logSpy);
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
// Should store the whole value (not just the hydrated parts)
|
||||
storeAsGlobal(id, ['props', 'nestedObject']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
|
||||
expect(console.log).toHaveBeenCalledWith('$reactTemp1');
|
||||
expect(global.$reactTemp1).toBe(nestedObject);
|
||||
|
||||
logSpy.mockReset();
|
||||
console.log.mockReset();
|
||||
|
||||
// Should store the nested property specified (not just the outer value)
|
||||
storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
|
||||
expect(console.log).toHaveBeenCalledWith('$reactTemp2');
|
||||
expect(global.$reactTemp2).toBe(nestedObject.a.b);
|
||||
|
||||
done();
|
||||
@@ -1805,4 +1809,421 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
describe('inline errors and warnings', () => {
|
||||
// Some actions require the Fiber id.
|
||||
// In those instances you might want to make assertions based on the ID instead of the index.
|
||||
function getErrorsAndWarningsForElement(id: number) {
|
||||
const index = ((store.getIndexOfElementID(id): any): number);
|
||||
return getErrorsAndWarningsForElementAtIndex(index);
|
||||
}
|
||||
|
||||
async function getErrorsAndWarningsForElementAtIndex(index) {
|
||||
const id = ((store.getElementIDAtIndex(index): any): number);
|
||||
|
||||
let errors = null;
|
||||
let warnings = null;
|
||||
|
||||
function Suspender({target}) {
|
||||
const {getInspectedElement} = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = getInspectedElement(id);
|
||||
errors = inspectedElement.errors;
|
||||
warnings = inspectedElement.warnings;
|
||||
return null;
|
||||
}
|
||||
|
||||
let root;
|
||||
await utils.actAsync(() => {
|
||||
root = TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={index}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
);
|
||||
}, false);
|
||||
await utils.actAsync(() => {
|
||||
root.unmount();
|
||||
}, false);
|
||||
|
||||
return {errors, warnings};
|
||||
}
|
||||
|
||||
it('during render get recorded', async () => {
|
||||
const Example = () => {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
await withErrorsOrWarningsIgnored(['test-only: '], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
});
|
||||
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning",
|
||||
1,
|
||||
],
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('during render get deduped', async () => {
|
||||
const Example = () => {
|
||||
console.error('test-only: render error');
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
console.warn('test-only: render warning');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
});
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error",
|
||||
2,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning",
|
||||
3,
|
||||
],
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('during layout (mount) get recorded', async () => {
|
||||
const Example = () => {
|
||||
// Note we only test mount because once the component unmounts,
|
||||
// it is no longer in the store and warnings are ignored.
|
||||
React.useLayoutEffect(() => {
|
||||
console.error('test-only: useLayoutEffect error');
|
||||
console.warn('test-only: useLayoutEffect warning');
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
});
|
||||
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: useLayoutEffect error",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: useLayoutEffect warning",
|
||||
1,
|
||||
],
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('during passive (mount) get recorded', async () => {
|
||||
const Example = () => {
|
||||
// Note we only test mount because once the component unmounts,
|
||||
// it is no longer in the store and warnings are ignored.
|
||||
React.useEffect(() => {
|
||||
console.error('test-only: useEffect error');
|
||||
console.warn('test-only: useEffect warning');
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
});
|
||||
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: useEffect error",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: useEffect warning",
|
||||
1,
|
||||
],
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('from react get recorded without a component stack', async () => {
|
||||
const Example = () => {
|
||||
return [<div />];
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(
|
||||
['Warning: Each child in a list should have a unique "key" prop.'],
|
||||
async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"Warning: Each child in a list should have a unique \\"key\\" prop. See https://reactjs.org/link/warning-keys for more information.",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for the whole app', async () => {
|
||||
const Example = () => {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example repeatWarningCount={1} />, container),
|
||||
);
|
||||
});
|
||||
|
||||
store.clearErrorsAndWarnings();
|
||||
// Flush events to the renderer.
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
const data = await getErrorsAndWarningsForElementAtIndex(0);
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"errors": Array [],
|
||||
"warnings": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for a particular Fiber (only errors)', async () => {
|
||||
const Example = ({id}) => {
|
||||
console.error(`test-only: render error #${id}`);
|
||||
console.warn(`test-only: render warning #${id}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Example id={1} />
|
||||
<Example id={2} />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
store.clearWarningsForElement(2);
|
||||
// Flush events to the renderer.
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
let data = [
|
||||
await getErrorsAndWarningsForElement(1),
|
||||
await getErrorsAndWarningsForElement(2),
|
||||
];
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error #2",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
store.clearWarningsForElement(1);
|
||||
// Flush events to the renderer.
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
data = [
|
||||
await getErrorsAndWarningsForElement(1),
|
||||
await getErrorsAndWarningsForElement(2),
|
||||
];
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [],
|
||||
},
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error #2",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for a particular Fiber (only warnings)', async () => {
|
||||
const Example = ({id}) => {
|
||||
console.error(`test-only: render error #${id}`);
|
||||
console.warn(`test-only: render warning #${id}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.withErrorsOrWarningsIgnored(['test-only:'], async () => {
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Example id={1} />
|
||||
<Example id={2} />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
store.clearErrorsForElement(2);
|
||||
// Flush events to the renderer.
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
let data = [
|
||||
await getErrorsAndWarningsForElement(1),
|
||||
await getErrorsAndWarningsForElement(2),
|
||||
];
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"errors": Array [
|
||||
Array [
|
||||
"test-only: render error #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"errors": Array [],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning #2",
|
||||
1,
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
store.clearErrorsForElement(1);
|
||||
// Flush events to the renderer.
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
data = [
|
||||
await getErrorsAndWarningsForElement(1),
|
||||
await getErrorsAndWarningsForElement(2),
|
||||
];
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"errors": Array [],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning #1",
|
||||
1,
|
||||
],
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"errors": Array [],
|
||||
"warnings": Array [
|
||||
Array [
|
||||
"test-only: render warning #2",
|
||||
1,
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,4 +120,19 @@ describe('ProfilerStore', () => {
|
||||
expect(data.commitData).toHaveLength(1);
|
||||
expect(data.operations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw if component filters are modified while profiling', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
|
||||
expect(() => {
|
||||
utils.act(() => {
|
||||
const {
|
||||
ElementTypeHostComponent,
|
||||
} = require('react-devtools-shared/src/types');
|
||||
store.componentFilters = [
|
||||
utils.createElementTypeFilter(ElementTypeHostComponent),
|
||||
];
|
||||
});
|
||||
}).toThrow('Cannot modify filter preferences while profiling');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,11 +33,24 @@ env.beforeEach(() => {
|
||||
const {
|
||||
getDefaultComponentFilters,
|
||||
saveComponentFilters,
|
||||
setShowInlineWarningsAndErrors,
|
||||
} = require('react-devtools-shared/src/utils');
|
||||
|
||||
// Fake timers let us flush Bridge operations between setup and assertions.
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array.
|
||||
global._ignoredErrorOrWarningMessages = [];
|
||||
function shouldIgnoreConsoleErrorOrWarn(args) {
|
||||
const firstArg = args[0];
|
||||
if (typeof firstArg !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
|
||||
return firstArg.indexOf(errorOrWarningMessage) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
// $FlowFixMe
|
||||
console.error = (...args) => {
|
||||
@@ -54,14 +67,32 @@ env.beforeEach(() => {
|
||||
// DevTools intentionally wraps updates with acts from both DOM and test-renderer,
|
||||
// since test updates are expected to impact both renderers.
|
||||
return;
|
||||
} else if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||
// Allows testing how DevTools behaves when it encounters console.error without cluttering the test output.
|
||||
// Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
|
||||
return;
|
||||
}
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
const originalConsoleWarn = console.warn;
|
||||
// $FlowFixMe
|
||||
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;
|
||||
}
|
||||
originalConsoleWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Initialize filters to a known good state.
|
||||
saveComponentFilters(getDefaultComponentFilters());
|
||||
global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters();
|
||||
|
||||
// Also initialize inline warnings so that we can test them.
|
||||
setShowInlineWarningsAndErrors(true);
|
||||
global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
|
||||
|
||||
installHook(global);
|
||||
|
||||
const bridgeListeners = [];
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('Store', () => {
|
||||
let act;
|
||||
let getRendererID;
|
||||
let store;
|
||||
let withErrorsOrWarningsIgnored;
|
||||
|
||||
beforeEach(() => {
|
||||
agent = global.agent;
|
||||
@@ -25,6 +26,7 @@ describe('Store', () => {
|
||||
const utils = require('./utils');
|
||||
act = utils.act;
|
||||
getRendererID = utils.getRendererID;
|
||||
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
|
||||
});
|
||||
|
||||
it('should not allow a root node to be collapsed', () => {
|
||||
@@ -1008,4 +1010,316 @@ describe('Store', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('inline errors and warnings', () => {
|
||||
it('during render are counted', () => {
|
||||
function Example() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 1
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example rerender={1} />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
});
|
||||
|
||||
it('during layout get counted', () => {
|
||||
function Example() {
|
||||
React.useLayoutEffect(() => {
|
||||
console.error('test-only: layout error');
|
||||
console.warn('test-only: layout warning');
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 1
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example rerender={1} />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
});
|
||||
|
||||
// This is not great, but it seems safer than potentially flushing between commits.
|
||||
// Our logic for determining how to handle e.g. suspended trees or error boundaries
|
||||
// is built on the assumption that we're evaluating the results of a commit, not an in-progress render.
|
||||
it('during passive get counted (but not until the next commit)', () => {
|
||||
function Example() {
|
||||
React.useEffect(() => {
|
||||
console.error('test-only: passive error');
|
||||
console.warn('test-only: passive warning');
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Example>
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<Example rerender={1} />, container));
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 1
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
act(() => ReactDOM.unmountComponentAtNode(container));
|
||||
expect(store).toMatchInlineSnapshot(``);
|
||||
});
|
||||
|
||||
it('from react get counted', () => {
|
||||
const container = document.createElement('div');
|
||||
function Example() {
|
||||
return [<Child />];
|
||||
}
|
||||
function Child() {
|
||||
return null;
|
||||
}
|
||||
|
||||
withErrorsOrWarningsIgnored(
|
||||
['Warning: Each child in a list should have a unique "key" prop'],
|
||||
() => {
|
||||
act(() => ReactDOM.render(<Example />, container));
|
||||
},
|
||||
);
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 0
|
||||
[root]
|
||||
▾ <Example> ✕
|
||||
<Child>
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for the whole app', () => {
|
||||
function Example() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Example />
|
||||
<Example />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
store.clearErrorsAndWarnings();
|
||||
// flush events to the renderer
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Example>
|
||||
<Example>
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for particular Fiber (only warnings)', () => {
|
||||
function Example() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Example />
|
||||
<Example />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
store.clearWarningsForElement(2);
|
||||
// Flush events to the renderer.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 1
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
<Example> ✕
|
||||
`);
|
||||
});
|
||||
|
||||
it('can be cleared for a particular Fiber (only errors)', () => {
|
||||
function Example() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Example />
|
||||
<Example />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
<Example> ✕⚠
|
||||
`);
|
||||
|
||||
store.clearErrorsForElement(2);
|
||||
// Flush events to the renderer.
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 2
|
||||
[root]
|
||||
<Example> ✕⚠
|
||||
<Example> ⚠
|
||||
`);
|
||||
});
|
||||
|
||||
it('are updated when fibers are removed from the tree', () => {
|
||||
function ComponentWithWarning() {
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
function ComponentWithError() {
|
||||
console.error('test-only: render error');
|
||||
return null;
|
||||
}
|
||||
function ComponentWithWarningAndError() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<ComponentWithError />
|
||||
<ComponentWithWarning />
|
||||
<ComponentWithWarningAndError />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<ComponentWithError> ✕
|
||||
<ComponentWithWarning> ⚠
|
||||
<ComponentWithWarningAndError> ✕⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<ComponentWithWarning />
|
||||
<ComponentWithWarningAndError />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 2
|
||||
[root]
|
||||
<ComponentWithWarning> ⚠
|
||||
<ComponentWithWarningAndError> ✕⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<ComponentWithWarning />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 2
|
||||
[root]
|
||||
<ComponentWithWarning> ⚠
|
||||
`);
|
||||
|
||||
withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(() => ReactDOM.render(<React.Fragment />, container));
|
||||
});
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
expect(store.errorCount).toBe(0);
|
||||
expect(store.warningCount).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+225
-34
@@ -48,22 +48,28 @@ describe('Store component filters', () => {
|
||||
});
|
||||
|
||||
it('should support filtering by element type', () => {
|
||||
class Root extends React.Component<{|children: React$Node|}> {
|
||||
class ClassComponent extends React.Component<{|children: React$Node|}> {
|
||||
render() {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
const Component = () => <div>Hi</div>;
|
||||
const FunctionComponent = () => <div>Hi</div>;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Root>
|
||||
<Component />
|
||||
</Root>,
|
||||
<ClassComponent>
|
||||
<FunctionComponent />
|
||||
</ClassComponent>,
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <ClassComponent>
|
||||
▾ <div>
|
||||
▾ <FunctionComponent>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -71,8 +77,11 @@ describe('Store component filters', () => {
|
||||
utils.createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('2: hide host components');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <ClassComponent>
|
||||
<FunctionComponent>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -80,8 +89,12 @@ describe('Store component filters', () => {
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('3: hide class components');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <div>
|
||||
▾ <FunctionComponent>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -90,8 +103,11 @@ describe('Store component filters', () => {
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('4: hide class and function components');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <div>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -100,15 +116,33 @@ describe('Store component filters', () => {
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
]),
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <ClassComponent>
|
||||
▾ <div>
|
||||
▾ <FunctionComponent>
|
||||
<div>
|
||||
`);
|
||||
|
||||
expect(store).toMatchSnapshot('5: disable all filters');
|
||||
act(() => (store.componentFilters = []));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <ClassComponent>
|
||||
▾ <div>
|
||||
▾ <FunctionComponent>
|
||||
<div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should ignore invalid ElementTypeRoot filter', () => {
|
||||
const Root = () => <div>Hi</div>;
|
||||
const Component = () => <div>Hi</div>;
|
||||
|
||||
act(() => ReactDOM.render(<Root />, document.createElement('div')));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
act(() => ReactDOM.render(<Component />, document.createElement('div')));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -117,7 +151,11 @@ describe('Store component filters', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('2: add invalid filter');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should filter by display name', () => {
|
||||
@@ -136,27 +174,59 @@ describe('Store component filters', () => {
|
||||
document.createElement('div'),
|
||||
),
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`);
|
||||
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('Foo')]),
|
||||
);
|
||||
expect(store).toMatchSnapshot('2: filter "Foo"');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`);
|
||||
|
||||
act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
|
||||
expect(store).toMatchSnapshot('3: filter "Ba"');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
<Text>
|
||||
<Text>
|
||||
`);
|
||||
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('B.z')]),
|
||||
);
|
||||
expect(store).toMatchSnapshot('4: filter "B.z"');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
<Text>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should filter by path', () => {
|
||||
const Component = () => <div>Hi</div>;
|
||||
|
||||
act(() => ReactDOM.render(<Component />, document.createElement('div')));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -165,9 +235,7 @@ describe('Store component filters', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot(
|
||||
'2: hide all components declared within this test filed',
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
@@ -176,7 +244,11 @@ describe('Store component filters', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('3: hide components in a made up fake path');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should filter HOCs', () => {
|
||||
@@ -187,15 +259,29 @@ describe('Store component filters', () => {
|
||||
Bar.displayName = 'Bar(Foo(Component))';
|
||||
|
||||
act(() => ReactDOM.render(<Bar />, document.createElement('div')));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component> [Bar][Foo]
|
||||
▾ <Component> [Foo]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(() => (store.componentFilters = [utils.createHOCFilter(true)]));
|
||||
|
||||
expect(store).toMatchSnapshot('2: hide all HOCs');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(() => (store.componentFilters = [utils.createHOCFilter(false)]));
|
||||
|
||||
expect(store).toMatchSnapshot('3: disable HOC filter');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Component> [Bar][Foo]
|
||||
▾ <Component> [Foo]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not send a bridge update if the set of enabled filters has not changed', () => {
|
||||
@@ -252,12 +338,117 @@ describe('Store component filters', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
|
||||
expect(store).toMatchSnapshot('1: suspended');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
▾ <Loading>
|
||||
<div>
|
||||
`);
|
||||
|
||||
act(() => ReactDOM.render(<Wrapper shouldSuspend={false} />, container));
|
||||
expect(store).toMatchSnapshot('2: resolved');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
<Component>
|
||||
`);
|
||||
|
||||
act(() => ReactDOM.render(<Wrapper shouldSuspend={true} />, container));
|
||||
expect(store).toMatchSnapshot('3: suspended');
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
▾ <Loading>
|
||||
<div>
|
||||
`);
|
||||
});
|
||||
|
||||
describe('inline errors and warnings', () => {
|
||||
it('only counts for unfiltered components', () => {
|
||||
function ComponentWithWarning() {
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
function ComponentWithError() {
|
||||
console.error('test-only: render error');
|
||||
return null;
|
||||
}
|
||||
function ComponentWithWarningAndError() {
|
||||
console.error('test-only: render error');
|
||||
console.warn('test-only: render warning');
|
||||
return null;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
utils.withErrorsOrWarningsIgnored(['test-only:'], () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
utils.createDisplayNameFilter('Warning'),
|
||||
utils.createDisplayNameFilter('Error'),
|
||||
]),
|
||||
);
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<ComponentWithError />
|
||||
<ComponentWithWarning />
|
||||
<ComponentWithWarningAndError />
|
||||
</React.Fragment>,
|
||||
container,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
expect(store.errorCount).toBe(0);
|
||||
expect(store.warningCount).toBe(0);
|
||||
|
||||
act(() => (store.componentFilters = []));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<ComponentWithError> ✕
|
||||
<ComponentWithWarning> ⚠
|
||||
<ComponentWithWarningAndError> ✕⚠
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [utils.createDisplayNameFilter('Warning')]),
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 1, ⚠ 0
|
||||
[root]
|
||||
<ComponentWithError> ✕
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [utils.createDisplayNameFilter('Error')]),
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 0, ⚠ 1
|
||||
[root]
|
||||
<ComponentWithWarning> ⚠
|
||||
`);
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
utils.createDisplayNameFilter('Warning'),
|
||||
utils.createDisplayNameFilter('Error'),
|
||||
]),
|
||||
);
|
||||
expect(store).toMatchInlineSnapshot(`[root]`);
|
||||
expect(store.errorCount).toBe(0);
|
||||
expect(store.warningCount).toBe(0);
|
||||
|
||||
act(() => (store.componentFilters = []));
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
✕ 2, ⚠ 2
|
||||
[root]
|
||||
<ComponentWithError> ✕
|
||||
<ComponentWithWarning> ⚠
|
||||
<ComponentWithWarningAndError> ✕⚠
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1988
-187
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
import {printStore} from 'react-devtools-shared/src/devtools/utils';
|
||||
|
||||
// test() is part of Jest's serializer API
|
||||
export function test(maybeState) {
|
||||
if (maybeState === null || typeof maybeState !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Duck typing at its finest.
|
||||
return (
|
||||
maybeState.hasOwnProperty('inspectedElementID') &&
|
||||
maybeState.hasOwnProperty('ownerFlatTree') &&
|
||||
maybeState.hasOwnProperty('ownerSubtreeLeafElementID')
|
||||
);
|
||||
}
|
||||
|
||||
// print() is part of Jest's serializer API
|
||||
export function print(state, serialize, indent) {
|
||||
// This is a big of a hack but it works around having to pass in a meta object e.g. {store, state}.
|
||||
// DevTools tests depend on a global Store object anyway (initialized via setupTest).
|
||||
const store = global.store;
|
||||
|
||||
return printStore(store, false, state);
|
||||
}
|
||||
@@ -213,3 +213,37 @@ export function exportImportHelper(bridge: FrontendBridge, store: Store): void {
|
||||
profilerStore.profilingData = profilingDataFrontend;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `fn` while preventing console error and warnings that partially match any given `errorOrWarningMessages` from appearing in the console.
|
||||
* @param errorOrWarningMessages Messages are matched partially (i.e. indexOf), pre-formatting.
|
||||
* @param fn
|
||||
*/
|
||||
export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
|
||||
errorOrWarningMessages: string[],
|
||||
fn: () => T,
|
||||
): T {
|
||||
let resetIgnoredErrorOrWarningMessages = true;
|
||||
try {
|
||||
global._ignoredErrorOrWarningMessages = errorOrWarningMessages;
|
||||
const maybeThenable = fn();
|
||||
if (
|
||||
maybeThenable !== undefined &&
|
||||
typeof maybeThenable.then === 'function'
|
||||
) {
|
||||
resetIgnoredErrorOrWarningMessages = false;
|
||||
return maybeThenable.then(
|
||||
() => {
|
||||
global._ignoredErrorOrWarningMessages = [];
|
||||
},
|
||||
() => {
|
||||
global._ignoredErrorOrWarningMessages = [];
|
||||
},
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (resetIgnoredErrorOrWarningMessages) {
|
||||
global._ignoredErrorOrWarningMessages = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-1
@@ -169,6 +169,9 @@ export default class Agent extends EventEmitter<{|
|
||||
|
||||
this._bridge = bridge;
|
||||
|
||||
bridge.addListener('clearErrorsAndWarnings', this.clearErrorsAndWarnings);
|
||||
bridge.addListener('clearErrorsForFiberID', this.clearErrorsForFiberID);
|
||||
bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
|
||||
bridge.addListener('copyElementPath', this.copyElementPath);
|
||||
bridge.addListener('deletePath', this.deletePath);
|
||||
bridge.addListener('getProfilingData', this.getProfilingData);
|
||||
@@ -226,6 +229,33 @@ export default class Agent extends EventEmitter<{|
|
||||
return this._rendererInterfaces;
|
||||
}
|
||||
|
||||
clearErrorsAndWarnings = ({rendererID}: {|rendererID: RendererID|}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
renderer.clearErrorsAndWarnings();
|
||||
}
|
||||
};
|
||||
|
||||
clearErrorsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
renderer.clearErrorsForFiberID(id);
|
||||
}
|
||||
};
|
||||
|
||||
clearWarningsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
renderer.clearWarningsForFiberID(id);
|
||||
}
|
||||
};
|
||||
|
||||
copyElementPath = ({id, path, rendererID}: CopyElementParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
@@ -571,16 +601,22 @@ export default class Agent extends EventEmitter<{|
|
||||
updateConsolePatchSettings = ({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
}: {|
|
||||
appendComponentStack: boolean,
|
||||
breakOnConsoleErrors: boolean,
|
||||
showInlineWarningsAndErrors: boolean,
|
||||
|}) => {
|
||||
// If the frontend preference has change,
|
||||
// or in the case of React Native- if the backend is just finding out the preference-
|
||||
// then install or uninstall the console overrides.
|
||||
// It's safe to call these methods multiple times, so we don't need to worry about that.
|
||||
if (appendComponentStack || breakOnConsoleErrors) {
|
||||
patchConsole({appendComponentStack, breakOnConsoleErrors});
|
||||
patchConsole({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
});
|
||||
} else {
|
||||
unpatchConsole();
|
||||
}
|
||||
|
||||
+69
-29
@@ -22,11 +22,22 @@ const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
|
||||
// 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);
|
||||
}
|
||||
|
||||
type OnErrorOrWarning = (
|
||||
fiber: Fiber,
|
||||
type: 'error' | 'warn',
|
||||
args: Array<any>,
|
||||
) => void;
|
||||
|
||||
const injectedRenderers: Map<
|
||||
ReactRenderer,
|
||||
{|
|
||||
currentDispatcherRef: CurrentDispatcherRef,
|
||||
getCurrentFiber: () => Fiber | null,
|
||||
onErrorOrWarning: ?OnErrorOrWarning,
|
||||
workTagMap: WorkTagMap,
|
||||
|},
|
||||
> = new Map();
|
||||
@@ -54,7 +65,10 @@ export function dangerous_setTargetConsoleForTesting(
|
||||
// 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(renderer: ReactRenderer): void {
|
||||
export function registerRenderer(
|
||||
renderer: ReactRenderer,
|
||||
onErrorOrWarning?: OnErrorOrWarning,
|
||||
): void {
|
||||
const {
|
||||
currentDispatcherRef,
|
||||
getCurrentFiber,
|
||||
@@ -76,6 +90,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
|
||||
currentDispatcherRef,
|
||||
getCurrentFiber,
|
||||
workTagMap: ReactTypeOfWork,
|
||||
onErrorOrWarning,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -83,6 +98,7 @@ export function registerRenderer(renderer: ReactRenderer): void {
|
||||
const consoleSettingsRef = {
|
||||
appendComponentStack: false,
|
||||
breakOnConsoleErrors: false,
|
||||
showInlineWarningsAndErrors: false,
|
||||
};
|
||||
|
||||
// Patches console methods to append component stack for the current fiber.
|
||||
@@ -90,14 +106,17 @@ const consoleSettingsRef = {
|
||||
export function patch({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
}: {
|
||||
appendComponentStack: boolean,
|
||||
breakOnConsoleErrors: boolean,
|
||||
showInlineWarningsAndErrors: boolean,
|
||||
}): 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;
|
||||
|
||||
if (unpatchFn !== null) {
|
||||
// Don't patch twice.
|
||||
@@ -121,32 +140,51 @@ export function patch({
|
||||
targetConsole[method]);
|
||||
|
||||
const overrideMethod = (...args) => {
|
||||
const latestAppendComponentStack =
|
||||
consoleSettingsRef.appendComponentStack;
|
||||
const latestBreakOnConsoleErrors =
|
||||
consoleSettingsRef.breakOnConsoleErrors;
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
const alreadyHasComponentStack =
|
||||
lastArg !== null && isStringComponentStack(lastArg);
|
||||
|
||||
if (latestAppendComponentStack) {
|
||||
try {
|
||||
// If we are ever called with a string that already has a component stack, e.g. a React error/warning,
|
||||
// don't append a second stack.
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
const alreadyHasComponentStack =
|
||||
lastArg !== null &&
|
||||
(PREFIX_REGEX.test(lastArg) ||
|
||||
ROW_COLUMN_NUMBER_REGEX.test(lastArg));
|
||||
let shouldAppendWarningStack = false;
|
||||
if (consoleSettingsRef.appendComponentStack) {
|
||||
// If we are ever called with a string that already has a component stack,
|
||||
// e.g. a React error/warning, don't append a second stack.
|
||||
shouldAppendWarningStack = !alreadyHasComponentStack;
|
||||
}
|
||||
|
||||
if (!alreadyHasComponentStack) {
|
||||
// If there's a component stack for at least one of the injected renderers, append it.
|
||||
// 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 {
|
||||
currentDispatcherRef,
|
||||
getCurrentFiber,
|
||||
workTagMap,
|
||||
} of injectedRenderers.values()) {
|
||||
const current: ?Fiber = getCurrentFiber();
|
||||
if (current != null) {
|
||||
const shouldShowInlineWarningsAndErrors =
|
||||
consoleSettingsRef.showInlineWarningsAndErrors &&
|
||||
(method === 'error' || method === 'warn');
|
||||
|
||||
if (shouldAppendWarningStack || shouldShowInlineWarningsAndErrors) {
|
||||
// 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 {
|
||||
currentDispatcherRef,
|
||||
getCurrentFiber,
|
||||
onErrorOrWarning,
|
||||
workTagMap,
|
||||
} of injectedRenderers.values()) {
|
||||
const current: ?Fiber = getCurrentFiber();
|
||||
if (current != null) {
|
||||
try {
|
||||
if (shouldShowInlineWarningsAndErrors) {
|
||||
// patch() is called by two places: (1) the hook and (2) the renderer backend.
|
||||
// The backend is what impliments a message queue, so it's the only one that injects onErrorOrWarning.
|
||||
if (typeof onErrorOrWarning === 'function') {
|
||||
onErrorOrWarning(
|
||||
current,
|
||||
((method: any): 'error' | 'warn'),
|
||||
// Copy args before we mutate them (e.g. adding the component stack)
|
||||
alreadyHasComponentStack
|
||||
? // Replace component stack with an empty string in case there's a string placeholder for it.
|
||||
[...args.slice(0, -1), '']
|
||||
: args.slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAppendWarningStack) {
|
||||
const componentStack = getStackByFiberInDevAndProd(
|
||||
workTagMap,
|
||||
current,
|
||||
@@ -155,16 +193,17 @@ export function patch({
|
||||
if (componentStack !== '') {
|
||||
args.push(componentStack);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
} finally {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
}
|
||||
}
|
||||
|
||||
if (latestBreakOnConsoleErrors) {
|
||||
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-
|
||||
@@ -177,6 +216,7 @@ export function patch({
|
||||
};
|
||||
|
||||
overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
|
||||
originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
|
||||
|
||||
// $FlowFixMe property error|warn is not writable.
|
||||
targetConsole[method] = overrideMethod;
|
||||
|
||||
@@ -779,6 +779,10 @@ export function attach(
|
||||
state = publicInstance.state || null;
|
||||
}
|
||||
|
||||
// Not implemented
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -812,6 +816,8 @@ export function attach(
|
||||
hooks: null,
|
||||
props,
|
||||
state,
|
||||
errors,
|
||||
warnings,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
@@ -1040,7 +1046,22 @@ export function attach(
|
||||
return null;
|
||||
}
|
||||
|
||||
function clearErrorsAndWarnings() {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
function clearErrorsForFiberID(id: number) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
function clearWarningsForFiberID(id: number) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
return {
|
||||
clearErrorsAndWarnings,
|
||||
clearErrorsForFiberID,
|
||||
clearWarningsForFiberID,
|
||||
cleanup,
|
||||
copyElementPath,
|
||||
deletePath,
|
||||
|
||||
+200
-4
@@ -49,7 +49,9 @@ import {
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REMOVE_ROOT,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../constants';
|
||||
import {inspectHooksOfFiber} from 'react-debug-tools';
|
||||
@@ -76,6 +78,7 @@ import {
|
||||
MEMO_NUMBER,
|
||||
MEMO_SYMBOL_STRING,
|
||||
} from './ReactSymbols';
|
||||
import {format} from './utils';
|
||||
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {
|
||||
@@ -117,6 +120,7 @@ type ReactTypeOfSideEffectType = {|
|
||||
NoFlags: number,
|
||||
PerformedWork: number,
|
||||
Placement: number,
|
||||
Incomplete: number,
|
||||
|};
|
||||
|
||||
function getFiberFlags(fiber: Fiber): number {
|
||||
@@ -143,6 +147,7 @@ export function getInternalReactConstants(
|
||||
NoFlags: 0b00,
|
||||
PerformedWork: 0b01,
|
||||
Placement: 0b10,
|
||||
Incomplete: 0b10000000000000,
|
||||
};
|
||||
|
||||
// **********************************************************
|
||||
@@ -479,7 +484,7 @@ export function attach(
|
||||
ReactTypeOfWork,
|
||||
ReactTypeOfSideEffect,
|
||||
} = getInternalReactConstants(renderer.version);
|
||||
const {NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
|
||||
const {Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
|
||||
const {
|
||||
FunctionComponent,
|
||||
ClassComponent,
|
||||
@@ -522,13 +527,104 @@ export function attach(
|
||||
typeof setSuspenseHandler === 'function' &&
|
||||
typeof scheduleUpdate === 'function';
|
||||
|
||||
// Set of Fibers (IDs) with recently changed number of error/warning messages.
|
||||
const fibersWithChangedErrorOrWarningCounts: Set<number> = new Set();
|
||||
|
||||
// Mapping of fiber IDs to error/warning messages and counts.
|
||||
const fiberToErrorsMap: Map<number, Map<string, number>> = new Map();
|
||||
const fiberToWarningsMap: Map<number, Map<string, number>> = new Map();
|
||||
|
||||
function clearErrorsAndWarnings() {
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const id of fiberToErrorsMap.keys()) {
|
||||
fibersWithChangedErrorOrWarningCounts.add(id);
|
||||
updateMostRecentlyInspectedElementIfNecessary(id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const id of fiberToWarningsMap.keys()) {
|
||||
fibersWithChangedErrorOrWarningCounts.add(id);
|
||||
updateMostRecentlyInspectedElementIfNecessary(id);
|
||||
}
|
||||
|
||||
fiberToErrorsMap.clear();
|
||||
fiberToWarningsMap.clear();
|
||||
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
function clearErrorsForFiberID(id: number) {
|
||||
if (fiberToErrorsMap.has(id)) {
|
||||
fiberToErrorsMap.delete(id);
|
||||
fibersWithChangedErrorOrWarningCounts.add(id);
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
updateMostRecentlyInspectedElementIfNecessary(id);
|
||||
}
|
||||
|
||||
function clearWarningsForFiberID(id: number) {
|
||||
if (fiberToWarningsMap.has(id)) {
|
||||
fiberToWarningsMap.delete(id);
|
||||
fibersWithChangedErrorOrWarningCounts.add(id);
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
updateMostRecentlyInspectedElementIfNecessary(id);
|
||||
}
|
||||
|
||||
function updateMostRecentlyInspectedElementIfNecessary(
|
||||
fiberID: number,
|
||||
): void {
|
||||
if (
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id === fiberID
|
||||
) {
|
||||
hasElementUpdatedSinceLastInspected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Called when an error or warning is logged during render, commit, or passive (including unmount functions).
|
||||
function onErrorOrWarning(
|
||||
fiber: Fiber,
|
||||
type: 'error' | 'warn',
|
||||
args: $ReadOnlyArray<any>,
|
||||
): void {
|
||||
const message = format(...args);
|
||||
|
||||
// Note that by calling these functions we may be creating the ID for the first time.
|
||||
// If the Fiber is then never mounted, we are responsible for cleaning up after ourselves.
|
||||
// This is important because getPrimaryFiber() stores a Fiber in the primaryFibers Set.
|
||||
// If a Fiber never mounts, and we don't clean up after this code, we could leak.
|
||||
// Fortunately we would only leak Fibers that have errors/warnings associated with them,
|
||||
// which is hopefully only a small set and only in DEV mode– but this is still not great.
|
||||
// We should clean up Fibers like this when flushing; see recordPendingErrorsAndWarnings().
|
||||
const fiberID = getFiberID(getPrimaryFiber(fiber));
|
||||
|
||||
// Mark this Fiber as needed its warning/error count updated during the next flush.
|
||||
fibersWithChangedErrorOrWarningCounts.add(fiberID);
|
||||
|
||||
// Update the error/warning messages and counts for the Fiber.
|
||||
const fiberMap = type === 'error' ? fiberToErrorsMap : fiberToWarningsMap;
|
||||
const messageMap = fiberMap.get(fiberID);
|
||||
if (messageMap != null) {
|
||||
const count = messageMap.get(message) || 0;
|
||||
messageMap.set(message, count + 1);
|
||||
} else {
|
||||
fiberMap.set(fiberID, new Map([[message, 1]]));
|
||||
}
|
||||
|
||||
// If this Fiber is currently being inspected, mark it as needing an udpate as well.
|
||||
updateMostRecentlyInspectedElementIfNecessary(fiberID);
|
||||
}
|
||||
|
||||
// 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)
|
||||
//
|
||||
// Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
registerRendererWithConsole(renderer);
|
||||
registerRendererWithConsole(renderer, onErrorOrWarning);
|
||||
|
||||
// The renderer interface can't read these preferences directly,
|
||||
// because it is stored in localStorage within the context of the extension.
|
||||
@@ -537,10 +633,13 @@ export function attach(
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
|
||||
const breakOnConsoleErrors =
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
|
||||
const showInlineWarningsAndErrors =
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
|
||||
if (appendComponentStack || breakOnConsoleErrors) {
|
||||
patchConsole({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -652,8 +751,11 @@ export function attach(
|
||||
// Recursively unmount all roots.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
unmountFiberChildrenRecursively(root.current);
|
||||
recordUnmount(root.current, false);
|
||||
// The TREE_OPERATION_REMOVE_ROOT operation serves two purposes:
|
||||
// 1. It avoids sending unnecessary bridge traffic to clear a root.
|
||||
// 2. It preserves Fiber IDs when remounting (below) which in turn ID to error/warning mapping.
|
||||
pushOperation(TREE_OPERATION_REMOVE_ROOT);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
|
||||
@@ -670,6 +772,10 @@ export function attach(
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
|
||||
// Also re-evaluate all error and warning counts given the new filters.
|
||||
reevaluateErrorsAndWarnings();
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with get*ForFiber methods
|
||||
@@ -1055,7 +1161,64 @@ export function attach(
|
||||
pendingOperations.push(op);
|
||||
}
|
||||
|
||||
function reevaluateErrorsAndWarnings() {
|
||||
fibersWithChangedErrorOrWarningCounts.clear();
|
||||
fiberToErrorsMap.forEach((countMap, fiberID) => {
|
||||
fibersWithChangedErrorOrWarningCounts.add(fiberID);
|
||||
});
|
||||
fiberToWarningsMap.forEach((countMap, fiberID) => {
|
||||
fibersWithChangedErrorOrWarningCounts.add(fiberID);
|
||||
});
|
||||
recordPendingErrorsAndWarnings();
|
||||
}
|
||||
|
||||
function recordPendingErrorsAndWarnings() {
|
||||
fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
|
||||
const fiber = idToFiberMap.get(fiberID);
|
||||
if (fiber != null) {
|
||||
// Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
|
||||
// We may also need to clean up after ourselves to avoid leaks.
|
||||
// See inline comments in onErrorOrWarning() for more info.
|
||||
if (isFiberMountedImpl(fiber) !== MOUNTED) {
|
||||
fiberToIDMap.delete(fiber);
|
||||
idToFiberMap.delete(fiberID);
|
||||
primaryFibers.delete(fiber);
|
||||
return;
|
||||
}
|
||||
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
|
||||
if (!shouldFilterFiber(fiber)) {
|
||||
const errorCountsMap = fiberToErrorsMap.get(fiberID);
|
||||
const warningCountsMap = fiberToWarningsMap.get(fiberID);
|
||||
|
||||
if (errorCountsMap != null) {
|
||||
errorCountsMap.forEach(count => {
|
||||
errorCount += count;
|
||||
});
|
||||
}
|
||||
if (warningCountsMap != null) {
|
||||
warningCountsMap.forEach(count => {
|
||||
warningCount += count;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
|
||||
pushOperation(fiberID);
|
||||
pushOperation(errorCount);
|
||||
pushOperation(warningCount);
|
||||
}
|
||||
});
|
||||
fibersWithChangedErrorOrWarningCounts.clear();
|
||||
}
|
||||
|
||||
function flushPendingEvents(root: Object): void {
|
||||
// Add any pending errors and warnings to the operations array.
|
||||
// We do this just before flushing, so we can ignore errors for no-longer-mounted Fibers.
|
||||
recordPendingErrorsAndWarnings();
|
||||
|
||||
if (
|
||||
pendingOperations.length === 0 &&
|
||||
pendingRealUnmountedIDs.length === 0 &&
|
||||
@@ -2028,17 +2191,42 @@ export function attach(
|
||||
// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberTreeReflection.js
|
||||
function isFiberMountedImpl(fiber: Fiber): number {
|
||||
let node = fiber;
|
||||
let prevNode = null;
|
||||
if (!fiber.alternate) {
|
||||
// If there is no alternate, this might be a new tree that isn't inserted
|
||||
// yet. If it is, then it will have a pending insertion effect on it.
|
||||
if ((getFiberFlags(node) & Placement) !== NoFlags) {
|
||||
return MOUNTING;
|
||||
}
|
||||
// This indicates an error during render.
|
||||
if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
|
||||
return UNMOUNTED;
|
||||
}
|
||||
while (node.return) {
|
||||
prevNode = node;
|
||||
node = node.return;
|
||||
|
||||
if ((getFiberFlags(node) & Placement) !== NoFlags) {
|
||||
return MOUNTING;
|
||||
}
|
||||
// This indicates an error during render.
|
||||
if ((getFiberFlags(node) & Incomplete) !== NoFlags) {
|
||||
return UNMOUNTED;
|
||||
}
|
||||
|
||||
// If this node is inside of a timed out suspense subtree, we should also ignore errors/warnings.
|
||||
const isTimedOutSuspense =
|
||||
node.tag === SuspenseComponent && node.memoizedState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Note that this does not include errors/warnings in the Fallback tree though!
|
||||
const primaryChildFragment = node.child;
|
||||
const fallbackChildFragment = primaryChildFragment
|
||||
? primaryChildFragment.sibling
|
||||
: null;
|
||||
if (prevNode !== fallbackChildFragment) {
|
||||
return UNMOUNTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (node.return) {
|
||||
@@ -2455,6 +2643,9 @@ export function attach(
|
||||
rootType = fiberRoot._debugRootType;
|
||||
}
|
||||
|
||||
const errors = fiberToErrorsMap.get(id) || new Map();
|
||||
const warnings = fiberToWarningsMap.get(id) || new Map();
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -2497,6 +2688,8 @@ export function attach(
|
||||
hooks,
|
||||
props: memoizedProps,
|
||||
state: usesHooks ? null : memoizedState,
|
||||
errors: Array.from(errors.entries()),
|
||||
warnings: Array.from(warnings.entries()),
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
@@ -3425,6 +3618,9 @@ export function attach(
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
clearErrorsAndWarnings,
|
||||
clearErrorsForFiberID,
|
||||
clearWarningsForFiberID,
|
||||
copyElementPath,
|
||||
deletePath,
|
||||
findNativeNodesForFiberID,
|
||||
|
||||
@@ -234,6 +234,8 @@ export type InspectedElement = {|
|
||||
props: Object | null,
|
||||
state: Object | null,
|
||||
key: number | string | null,
|
||||
errors: Array<[string, number]>,
|
||||
warnings: Array<[string, number]>,
|
||||
|
||||
// List of owners
|
||||
owners: Array<Owner> | null,
|
||||
@@ -294,6 +296,9 @@ type Type = 'props' | 'hooks' | 'state' | 'context';
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
clearErrorsAndWarnings: () => void,
|
||||
clearErrorsForFiberID: (id: number) => void,
|
||||
clearWarningsForFiberID: (id: number) => void,
|
||||
copyElementPath: (id: number, path: Array<string | number>) => void,
|
||||
deletePath: (
|
||||
type: Type,
|
||||
|
||||
@@ -134,3 +134,52 @@ export function serializeToString(data: any): string {
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
|
||||
// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
|
||||
// Implements s, d, i and f placeholders
|
||||
export function format(
|
||||
maybeMessage: any,
|
||||
...inputArgs: $ReadOnlyArray<any>
|
||||
): string {
|
||||
if (typeof maybeMessage !== 'string') {
|
||||
return [maybeMessage, ...inputArgs].join(' ');
|
||||
}
|
||||
|
||||
const re = /(%?)(%([jds]))/g;
|
||||
const args = inputArgs.slice();
|
||||
let formatted: string = maybeMessage;
|
||||
|
||||
if (args.length) {
|
||||
formatted = formatted.replace(re, (match, escaped, ptn, flag) => {
|
||||
let arg = args.shift();
|
||||
switch (flag) {
|
||||
case 's':
|
||||
arg += '';
|
||||
break;
|
||||
case 'd':
|
||||
case 'i':
|
||||
arg = parseInt(arg, 10).toString();
|
||||
break;
|
||||
case 'f':
|
||||
arg = parseFloat(arg).toString();
|
||||
break;
|
||||
}
|
||||
if (!escaped) {
|
||||
return arg;
|
||||
}
|
||||
args.unshift(arg);
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
// arguments remain after formatting
|
||||
if (args.length) {
|
||||
formatted += ' ' + args.join(' ');
|
||||
}
|
||||
|
||||
// update escaped %% values
|
||||
formatted = formatted.replace(/%{2,2}/g, '%');
|
||||
|
||||
return '' + formatted;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ type NativeStyleEditor_SetValueParams = {|
|
||||
type UpdateConsolePatchSettingsParams = {|
|
||||
appendComponentStack: boolean,
|
||||
breakOnConsoleErrors: boolean,
|
||||
showInlineWarningsAndErrors: boolean,
|
||||
|};
|
||||
|
||||
type BackendEvents = {|
|
||||
@@ -141,7 +142,10 @@ type BackendEvents = {|
|
||||
|};
|
||||
|
||||
type FrontendEvents = {|
|
||||
clearErrorsAndWarnings: [{|rendererID: RendererID|}],
|
||||
clearErrorsForFiberID: [ElementAndRendererID],
|
||||
clearNativeElementHighlight: [],
|
||||
clearWarningsForFiberID: [ElementAndRendererID],
|
||||
copyElementPath: [CopyElementPathParams],
|
||||
deletePath: [DeletePath],
|
||||
getOwnersList: [ElementAndRendererID],
|
||||
|
||||
@@ -14,6 +14,8 @@ export const TREE_OPERATION_ADD = 1;
|
||||
export const TREE_OPERATION_REMOVE = 2;
|
||||
export const TREE_OPERATION_REORDER_CHILDREN = 3;
|
||||
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
|
||||
export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
|
||||
export const TREE_OPERATION_REMOVE_ROOT = 6;
|
||||
|
||||
export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
|
||||
'React::DevTools::componentFilters';
|
||||
@@ -33,6 +35,9 @@ export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS =
|
||||
export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
|
||||
'React::DevTools::appendComponentStack';
|
||||
|
||||
export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY =
|
||||
'React::DevTools::showInlineWarningsAndErrors';
|
||||
|
||||
export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
|
||||
'React::DevTools::traceUpdatesEnabled';
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ import {inspect} from 'util';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REMOVE_ROOT,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../constants';
|
||||
import {ElementTypeRoot} from '../types';
|
||||
@@ -78,11 +80,22 @@ export default class Store extends EventEmitter<{|
|
||||
|}> {
|
||||
_bridge: FrontendBridge;
|
||||
|
||||
// Computed whenever _errorsAndWarnings Map changes.
|
||||
_cachedErrorCount: number = 0;
|
||||
_cachedWarningCount: number = 0;
|
||||
_cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
|
||||
|
||||
// Should new nodes be collapsed by default when added to the tree?
|
||||
_collapseNodesByDefault: boolean = true;
|
||||
|
||||
_componentFilters: Array<ComponentFilter>;
|
||||
|
||||
// Map of ID to number of recorded error and warning message IDs.
|
||||
_errorsAndWarnings: Map<
|
||||
number,
|
||||
{|errorCount: number, warningCount: number|},
|
||||
> = new Map();
|
||||
|
||||
// At least one of the injected renderers contains (DEV only) owner metadata.
|
||||
_hasOwnerMetadata: boolean = false;
|
||||
|
||||
@@ -289,6 +302,10 @@ export default class Store extends EventEmitter<{|
|
||||
this.emit('componentFilters');
|
||||
}
|
||||
|
||||
get errorCount(): number {
|
||||
return this._cachedErrorCount;
|
||||
}
|
||||
|
||||
get hasOwnerMetadata(): boolean {
|
||||
return this._hasOwnerMetadata;
|
||||
}
|
||||
@@ -357,6 +374,46 @@ export default class Store extends EventEmitter<{|
|
||||
return this._unsupportedRendererVersionDetected;
|
||||
}
|
||||
|
||||
get warningCount(): number {
|
||||
return this._cachedWarningCount;
|
||||
}
|
||||
|
||||
clearErrorsAndWarnings(): void {
|
||||
this._rootIDToRendererID.forEach(rendererID => {
|
||||
this._bridge.send('clearErrorsAndWarnings', {
|
||||
rendererID,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clearErrorsForElement(id: number): void {
|
||||
const rendererID = this.getRendererIDForElement(id);
|
||||
if (rendererID === null) {
|
||||
console.warn(
|
||||
`Unable to find rendererID for element ${id} when clearing errors.`,
|
||||
);
|
||||
} else {
|
||||
this._bridge.send('clearErrorsForFiberID', {
|
||||
rendererID,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearWarningsForElement(id: number): void {
|
||||
const rendererID = this.getRendererIDForElement(id);
|
||||
if (rendererID === null) {
|
||||
console.warn(
|
||||
`Unable to find rendererID for element ${id} when clearing warnings.`,
|
||||
);
|
||||
} else {
|
||||
this._bridge.send('clearWarningsForFiberID', {
|
||||
rendererID,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
containsElement(id: number): boolean {
|
||||
return this._idToElement.get(id) != null;
|
||||
}
|
||||
@@ -425,6 +482,17 @@ export default class Store extends EventEmitter<{|
|
||||
return element;
|
||||
}
|
||||
|
||||
// Returns a tuple of [id, index]
|
||||
getElementsWithErrorsAndWarnings(): Array<{|id: number, index: number|}> {
|
||||
return this._cachedErrorAndWarningTuples;
|
||||
}
|
||||
|
||||
getErrorAndWarningCountForElementID(
|
||||
id: number,
|
||||
): {|errorCount: number, warningCount: number|} {
|
||||
return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};
|
||||
}
|
||||
|
||||
getIndexOfElementID(id: number): number | null {
|
||||
const element = this.getElementByID(id);
|
||||
|
||||
@@ -709,6 +777,7 @@ export default class Store extends EventEmitter<{|
|
||||
}
|
||||
|
||||
let haveRootsChanged = false;
|
||||
let haveErrorsOrWarningsChanged = false;
|
||||
|
||||
// The first two values are always rendererID and rootID
|
||||
const rendererID = operations[0];
|
||||
@@ -910,9 +979,43 @@ export default class Store extends EventEmitter<{|
|
||||
set.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._errorsAndWarnings.has(id)) {
|
||||
this._errorsAndWarnings.delete(id);
|
||||
haveErrorsOrWarningsChanged = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_REMOVE_ROOT: {
|
||||
i += 1;
|
||||
|
||||
const id = operations[1];
|
||||
|
||||
if (__DEBUG__) {
|
||||
debug(`Remove root ${id}`);
|
||||
}
|
||||
|
||||
const recursivelyDeleteElements = elementID => {
|
||||
const element = this._idToElement.get(elementID);
|
||||
this._idToElement.delete(elementID);
|
||||
if (element) {
|
||||
// Mostly for Flow's sake
|
||||
for (let index = 0; index < element.children.length; index++) {
|
||||
recursivelyDeleteElements(element.children[index]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const root = ((this._idToElement.get(id): any): Element);
|
||||
recursivelyDeleteElements(id);
|
||||
|
||||
this._rootIDToCapabilities.delete(id);
|
||||
this._rootIDToRendererID.delete(id);
|
||||
this._roots = this._roots.filter(rootID => rootID !== id);
|
||||
this._weightAcrossRoots -= root.weight;
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_REORDER_CHILDREN: {
|
||||
const id = ((operations[i + 1]: any): number);
|
||||
const numChildren = ((operations[i + 2]: any): number);
|
||||
@@ -958,6 +1061,20 @@ export default class Store extends EventEmitter<{|
|
||||
// The profiler UI uses them lazily in order to generate the tree.
|
||||
i += 3;
|
||||
break;
|
||||
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
|
||||
const id = operations[i + 1];
|
||||
const errorCount = operations[i + 2];
|
||||
const warningCount = operations[i + 3];
|
||||
|
||||
i += 4;
|
||||
|
||||
if (errorCount > 0 || warningCount > 0) {
|
||||
this._errorsAndWarnings.set(id, {errorCount, warningCount});
|
||||
} else if (this._errorsAndWarnings.has(id)) {
|
||||
this._errorsAndWarnings.delete(id);
|
||||
}
|
||||
haveErrorsOrWarningsChanged = true;
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation ${operation}`);
|
||||
}
|
||||
@@ -965,6 +1082,41 @@ export default class Store extends EventEmitter<{|
|
||||
|
||||
this._revision++;
|
||||
|
||||
if (haveErrorsOrWarningsChanged) {
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
|
||||
this._errorsAndWarnings.forEach(entry => {
|
||||
errorCount += entry.errorCount;
|
||||
warningCount += entry.warningCount;
|
||||
});
|
||||
|
||||
this._cachedErrorCount = errorCount;
|
||||
this._cachedWarningCount = warningCount;
|
||||
|
||||
const errorAndWarningTuples: Array<{|id: number, index: number|}> = [];
|
||||
|
||||
this._errorsAndWarnings.forEach((_, id) => {
|
||||
const index = this.getIndexOfElementID(id);
|
||||
if (index !== null) {
|
||||
let low = 0;
|
||||
let high = errorAndWarningTuples.length;
|
||||
while (low < high) {
|
||||
const mid = (low + high) >> 1;
|
||||
if (errorAndWarningTuples[mid].index > index) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
errorAndWarningTuples.splice(low, 0, {id, index});
|
||||
}
|
||||
});
|
||||
|
||||
this._cachedErrorAndWarningTuples = errorAndWarningTuples;
|
||||
}
|
||||
|
||||
if (haveRootsChanged) {
|
||||
const prevSupportsProfiling = this._supportsProfiling;
|
||||
|
||||
|
||||
+83
-26
@@ -10,6 +10,7 @@
|
||||
import JSON5 from 'json5';
|
||||
|
||||
import type {Element} from './views/Components/types';
|
||||
import type {StateContext} from './views/Components/TreeContext';
|
||||
import type Store from './store';
|
||||
|
||||
export function printElement(element: Element, includeWeight: boolean = false) {
|
||||
@@ -49,39 +50,95 @@ export function printOwnersList(
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function printStore(store: Store, includeWeight: boolean = false) {
|
||||
export function printStore(
|
||||
store: Store,
|
||||
includeWeight: boolean = false,
|
||||
state: StateContext | null = null,
|
||||
) {
|
||||
const snapshotLines = [];
|
||||
|
||||
let rootWeight = 0;
|
||||
|
||||
store.roots.forEach(rootID => {
|
||||
const {weight} = ((store.getElementByID(rootID): any): Element);
|
||||
|
||||
snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : ''));
|
||||
|
||||
for (let i = rootWeight; i < rootWeight + weight; i++) {
|
||||
const element = store.getElementAtIndex(i);
|
||||
|
||||
if (element == null) {
|
||||
throw Error(`Could not find element at index ${i}`);
|
||||
}
|
||||
|
||||
snapshotLines.push(printElement(element, includeWeight));
|
||||
function printSelectedMarker(index: number): string {
|
||||
if (state === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
rootWeight += weight;
|
||||
});
|
||||
|
||||
// Make sure the pretty-printed test align with the Store's reported number of total rows.
|
||||
if (rootWeight !== store.numElements) {
|
||||
throw Error(
|
||||
`Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
|
||||
);
|
||||
return state.selectedElementIndex === index ? `→` : ' ';
|
||||
}
|
||||
|
||||
// If roots have been unmounted, verify that they've been removed from maps.
|
||||
// This helps ensure the Store doesn't leak memory.
|
||||
store.assertExpectedRootMapSizes();
|
||||
function printErrorsAndWarnings(element: Element): string {
|
||||
const {
|
||||
errorCount,
|
||||
warningCount,
|
||||
} = store.getErrorAndWarningCountForElementID(element.id);
|
||||
if (errorCount === 0 && warningCount === 0) {
|
||||
return '';
|
||||
}
|
||||
return ` ${errorCount > 0 ? '✕' : ''}${warningCount > 0 ? '⚠' : ''}`;
|
||||
}
|
||||
|
||||
const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
|
||||
if (ownerFlatTree !== null) {
|
||||
snapshotLines.push(
|
||||
'[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
|
||||
);
|
||||
ownerFlatTree.forEach((element, index) => {
|
||||
const printedSelectedMarker = printSelectedMarker(index);
|
||||
const printedElement = printElement(element, false);
|
||||
const printedErrorsAndWarnings = printErrorsAndWarnings(element);
|
||||
snapshotLines.push(
|
||||
`${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const errorsAndWarnings = store._errorsAndWarnings;
|
||||
if (errorsAndWarnings.size > 0) {
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
errorsAndWarnings.forEach(entry => {
|
||||
errorCount += entry.errorCount;
|
||||
warningCount += entry.warningCount;
|
||||
});
|
||||
|
||||
snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`);
|
||||
}
|
||||
|
||||
store.roots.forEach(rootID => {
|
||||
const {weight} = ((store.getElementByID(rootID): any): Element);
|
||||
const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
|
||||
|
||||
// Store does not (yet) expose a way to get errors/warnings per root.
|
||||
snapshotLines.push(`[root]${maybeWeightLabel}`);
|
||||
|
||||
for (let i = rootWeight; i < rootWeight + weight; i++) {
|
||||
const element = store.getElementAtIndex(i);
|
||||
|
||||
if (element == null) {
|
||||
throw Error(`Could not find element at index ${i}`);
|
||||
}
|
||||
|
||||
const printedSelectedMarker = printSelectedMarker(i);
|
||||
const printedElement = printElement(element, includeWeight);
|
||||
const printedErrorsAndWarnings = printErrorsAndWarnings(element);
|
||||
snapshotLines.push(
|
||||
`${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
|
||||
);
|
||||
}
|
||||
|
||||
rootWeight += weight;
|
||||
});
|
||||
|
||||
// Make sure the pretty-printed test align with the Store's reported number of total rows.
|
||||
if (rootWeight !== store.numElements) {
|
||||
throw Error(
|
||||
`Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${store.numElements})`,
|
||||
);
|
||||
}
|
||||
|
||||
// If roots have been unmounted, verify that they've been removed from maps.
|
||||
// This helps ensure the Store doesn't leak memory.
|
||||
store.assertExpectedRootMapSizes();
|
||||
}
|
||||
|
||||
return snapshotLines.join('\n');
|
||||
}
|
||||
|
||||
@@ -73,3 +73,21 @@
|
||||
.Badge {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ErrorIcon,
|
||||
.ErrorIconContrast,
|
||||
.WarningIcon,
|
||||
.WarningIconContrast {
|
||||
height: 0.75rem !important;
|
||||
width: 0.75rem !important;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
.ErrorIcon {
|
||||
color: var(--color-console-error-icon);
|
||||
}
|
||||
.WarningIcon {
|
||||
color: var(--color-console-warning-icon);
|
||||
}
|
||||
.ErrorIconContrast, .WarningIconContrast {
|
||||
color: var(--color-component-name);
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ import Badge from './Badge';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {createRegExp} from '../utils';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {useSubscription} from '../hooks';
|
||||
|
||||
import type {ItemData} from './Tree';
|
||||
import type {Element} from './types';
|
||||
import type {Element as ElementType} from './types';
|
||||
|
||||
import styles from './Element.css';
|
||||
import Icon from '../Icon';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
@@ -28,12 +31,13 @@ type Props = {
|
||||
...
|
||||
};
|
||||
|
||||
export default function ElementView({data, index, style}: Props) {
|
||||
export default function Element({data, index, style}: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const {ownerFlatTree, ownerID, selectedElementID} = useContext(
|
||||
TreeStateContext,
|
||||
);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {showInlineWarningsAndErrors} = React.useContext(SettingsContext);
|
||||
|
||||
const element =
|
||||
ownerFlatTree !== null
|
||||
@@ -46,6 +50,24 @@ export default function ElementView({data, index, style}: Props) {
|
||||
const id = element === null ? null : element.id;
|
||||
const isSelected = selectedElementID === id;
|
||||
|
||||
const errorsAndWarningsSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () =>
|
||||
element === null
|
||||
? {errorCount: 0, warningCount: 0}
|
||||
: store.getErrorAndWarningCountForElementID(element.id),
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('mutated', callback);
|
||||
return () => store.removeListener('mutated', callback);
|
||||
},
|
||||
}),
|
||||
[store, element],
|
||||
);
|
||||
const {errorCount, warningCount} = useSubscription<{|
|
||||
errorCount: number,
|
||||
warningCount: number,
|
||||
|}>(errorsAndWarningsSubscription);
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (id !== null) {
|
||||
dispatch({type: 'SELECT_OWNER', payload: id});
|
||||
@@ -81,7 +103,7 @@ export default function ElementView({data, index, style}: Props) {
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
console.warn(`<ElementView> Could not find element at index ${index}`);
|
||||
console.warn(`<Element> Could not find element at index ${index}`);
|
||||
|
||||
// This return needs to happen after hooks, since hooks can't be conditional.
|
||||
return null;
|
||||
@@ -93,7 +115,7 @@ export default function ElementView({data, index, style}: Props) {
|
||||
hocDisplayNames,
|
||||
key,
|
||||
type,
|
||||
} = ((element: any): Element);
|
||||
} = ((element: any): ElementType);
|
||||
|
||||
let className = styles.Element;
|
||||
if (isSelected) {
|
||||
@@ -148,6 +170,26 @@ export default function ElementView({data, index, style}: Props) {
|
||||
/>
|
||||
</Badge>
|
||||
) : null}
|
||||
{showInlineWarningsAndErrors && errorCount > 0 && (
|
||||
<Icon
|
||||
type="error"
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.ErrorIconContrast
|
||||
: styles.ErrorIcon
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showInlineWarningsAndErrors && warningCount > 0 && (
|
||||
<Icon
|
||||
type="warning"
|
||||
className={
|
||||
isSelected && treeFocused
|
||||
? styles.WarningIconContrast
|
||||
: styles.WarningIcon
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -160,7 +202,7 @@ const swallowDoubleClick = event => {
|
||||
};
|
||||
|
||||
type ExpandCollapseToggleProps = {|
|
||||
element: Element,
|
||||
element: ElementType,
|
||||
store: Store,
|
||||
|};
|
||||
|
||||
|
||||
+18
@@ -51,10 +51,13 @@ export type GetInspectedElement = (
|
||||
id: number,
|
||||
) => InspectedElementFrontend | null;
|
||||
|
||||
type RefreshInspectedElement = () => void;
|
||||
|
||||
export type InspectedElementContextType = {|
|
||||
copyInspectedElementPath: CopyInspectedElementPath,
|
||||
getInspectedElementPath: GetInspectedElementPath,
|
||||
getInspectedElement: GetInspectedElement,
|
||||
refreshInspectedElement: RefreshInspectedElement,
|
||||
storeAsGlobal: StoreAsGlobal,
|
||||
|};
|
||||
|
||||
@@ -159,6 +162,15 @@ function InspectedElementContextController({children}: Props) {
|
||||
// would itself be blocked by the same render that suspends (waiting for the data).
|
||||
const {selectedElementID} = useContext(TreeStateContext);
|
||||
|
||||
const refreshInspectedElement = useCallback<RefreshInspectedElement>(() => {
|
||||
if (selectedElementID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('inspectElement', {id: selectedElementID, rendererID});
|
||||
}
|
||||
}
|
||||
}, [bridge, selectedElementID]);
|
||||
|
||||
const [
|
||||
currentlyInspectedElement,
|
||||
setCurrentlyInspectedElement,
|
||||
@@ -217,6 +229,8 @@ function InspectedElementContextController({children}: Props) {
|
||||
rootType,
|
||||
state,
|
||||
key,
|
||||
errors,
|
||||
warnings,
|
||||
} = ((data.value: any): InspectedElementBackend);
|
||||
|
||||
const inspectedElement: InspectedElementFrontend = {
|
||||
@@ -257,6 +271,8 @@ function InspectedElementContextController({children}: Props) {
|
||||
hooks: hydrateHelper(hooks),
|
||||
props: hydrateHelper(props),
|
||||
state: hydrateHelper(state),
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
|
||||
element = store.getElementByID(id);
|
||||
@@ -343,6 +359,7 @@ function InspectedElementContextController({children}: Props) {
|
||||
copyInspectedElementPath,
|
||||
getInspectedElement,
|
||||
getInspectedElementPath,
|
||||
refreshInspectedElement,
|
||||
storeAsGlobal,
|
||||
}),
|
||||
// InspectedElement is used to invalidate the cache and schedule an update with React.
|
||||
@@ -351,6 +368,7 @@ function InspectedElementContextController({children}: Props) {
|
||||
currentlyInspectedElement,
|
||||
getInspectedElement,
|
||||
getInspectedElementPath,
|
||||
refreshInspectedElement,
|
||||
storeAsGlobal,
|
||||
],
|
||||
);
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
.ErrorTree, .WarningTree {
|
||||
padding: 0.25rem 0 0 0;
|
||||
}
|
||||
|
||||
.HeaderRow {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.HeaderRow {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.Error, .Warning {
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Error {
|
||||
border-top: 1px solid var(--color-console-error-border);
|
||||
background-color: var(--color-console-error-background);
|
||||
color: var(--color-error-text);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.Warning {
|
||||
border-top: 1px solid var(--color-console-warning-border);
|
||||
background-color: var(--color-console-warning-background);
|
||||
color: var(--color-warning-text);
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.Message {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ErrorBadge,
|
||||
.WarningBadge {
|
||||
display: inline-block;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
flex: 0 0 0.75rem;
|
||||
line-height: 0.75rem;
|
||||
text-align: center;
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
font-size: var(--font-size-monospace-small);
|
||||
}
|
||||
|
||||
.ErrorBadge {
|
||||
background-color: var(--color-console-error-icon);
|
||||
color: var(--color-console-error-badge-text);
|
||||
}
|
||||
|
||||
.WarningBadge {
|
||||
background-color: var(--color-console-warning-icon);
|
||||
color: var(--color-console-warning-badge-text);
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Store from '../../store';
|
||||
import sharedStyles from './InspectedElementSharedStyles.css';
|
||||
import styles from './InspectedElementErrorsAndWarningsTree.css';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import {InspectedElementContext} from './InspectedElementContext';
|
||||
|
||||
import type {InspectedElement} from './types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
type Props = {|
|
||||
bridge: FrontendBridge,
|
||||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
|};
|
||||
|
||||
export default function InspectedElementErrorsAndWarningsTree({
|
||||
bridge,
|
||||
inspectedElement,
|
||||
store,
|
||||
}: Props) {
|
||||
const {refreshInspectedElement} = useContext(InspectedElementContext);
|
||||
|
||||
const {showInlineWarningsAndErrors} = useContext(SettingsContext);
|
||||
if (!showInlineWarningsAndErrors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {errors, warnings} = inspectedElement;
|
||||
|
||||
const clearErrors = () => {
|
||||
const {id} = inspectedElement;
|
||||
store.clearErrorsForElement(id);
|
||||
|
||||
// Immediately poll for updated data.
|
||||
// This avoids a delay between clicking the clear button and refreshing errors.
|
||||
// Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
|
||||
refreshInspectedElement();
|
||||
};
|
||||
|
||||
const clearWarnings = () => {
|
||||
const {id} = inspectedElement;
|
||||
store.clearWarningsForElement(id);
|
||||
|
||||
// Immediately poll for updated data.
|
||||
// This avoids a delay between clicking the clear button and refreshing warnings.
|
||||
// Ideally this would be done with useTranstion but that requires updating to a newer Cache strategy.
|
||||
refreshInspectedElement();
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{errors.length > 0 && (
|
||||
<Tree
|
||||
badgeClassName={styles.ErrorBadge}
|
||||
bridge={bridge}
|
||||
className={styles.ErrorTree}
|
||||
clearMessages={clearErrors}
|
||||
entries={errors}
|
||||
label="errors"
|
||||
messageClassName={styles.Error}
|
||||
/>
|
||||
)}
|
||||
{warnings.length > 0 && (
|
||||
<Tree
|
||||
badgeClassName={styles.WarningBadge}
|
||||
bridge={bridge}
|
||||
className={styles.WarningTree}
|
||||
clearMessages={clearWarnings}
|
||||
entries={warnings}
|
||||
label="warnings"
|
||||
messageClassName={styles.Warning}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type TreeProps = {|
|
||||
badgeClassName: string,
|
||||
actions: React$Node,
|
||||
className: string,
|
||||
clearMessages: () => {},
|
||||
entries: Array<[string, number]>,
|
||||
label: string,
|
||||
messageClassName: string,
|
||||
|};
|
||||
|
||||
function Tree({
|
||||
badgeClassName,
|
||||
actions,
|
||||
className,
|
||||
clearMessages,
|
||||
entries,
|
||||
label,
|
||||
messageClassName,
|
||||
}: TreeProps) {
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={`${sharedStyles.InspectedElementTree} ${className}`}>
|
||||
<div className={`${sharedStyles.HeaderRow} ${styles.HeaderRow}`}>
|
||||
<div className={sharedStyles.Header}>{label}</div>
|
||||
<Button
|
||||
onClick={clearMessages}
|
||||
title={`Clear all ${label} for this component`}>
|
||||
<ButtonIcon type="clear" />
|
||||
</Button>
|
||||
</div>
|
||||
{entries.map(([message, count], index) => (
|
||||
<ErrorOrWarningView
|
||||
key={`${label}-${index}`}
|
||||
badgeClassName={badgeClassName}
|
||||
className={messageClassName}
|
||||
count={count}
|
||||
message={message}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ErrorOrWarningViewProps = {|
|
||||
badgeClassName: string,
|
||||
className: string,
|
||||
count: number,
|
||||
message: string,
|
||||
|};
|
||||
|
||||
function ErrorOrWarningView({
|
||||
className,
|
||||
badgeClassName,
|
||||
count,
|
||||
message,
|
||||
}: ErrorOrWarningViewProps) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{count > 1 && <div className={badgeClassName}>{count}</div>}
|
||||
<div className={styles.Message} title={message}>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+2
@@ -12,6 +12,8 @@
|
||||
}
|
||||
|
||||
.Header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
+8
@@ -19,6 +19,7 @@ import ButtonIcon from '../ButtonIcon';
|
||||
import Icon from '../Icon';
|
||||
import HocBadges from './HocBadges';
|
||||
import InspectedElementContextTree from './InspectedElementContextTree';
|
||||
import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
|
||||
import InspectedElementHooksTree from './InspectedElementHooksTree';
|
||||
import InspectedElementPropsTree from './InspectedElementPropsTree';
|
||||
import InspectedElementStateTree from './InspectedElementStateTree';
|
||||
@@ -120,6 +121,13 @@ export default function InspectedElementView({
|
||||
store={store}
|
||||
/>
|
||||
|
||||
<InspectedElementErrorsAndWarningsTree
|
||||
bridge={bridge}
|
||||
getInspectedElementPath={getInspectedElementPath}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
/>
|
||||
|
||||
<NativeStyleEditor />
|
||||
|
||||
{showRenderedBy && (
|
||||
|
||||
+37
-35
@@ -85,42 +85,44 @@ export default function SearchInput(props: Props) {
|
||||
value={searchText}
|
||||
/>
|
||||
{!!searchText && (
|
||||
<span className={styles.IndexLabel}>
|
||||
{Math.min(searchIndex + 1, searchResults.length)} |{' '}
|
||||
{searchResults.length}
|
||||
</span>
|
||||
<React.Fragment>
|
||||
<span className={styles.IndexLabel}>
|
||||
{Math.min(searchIndex + 1, searchResults.length)} |{' '}
|
||||
{searchResults.length}
|
||||
</span>
|
||||
<div className={styles.LeftVRule} />
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
|
||||
title={
|
||||
<React.Fragment>
|
||||
Scroll to previous search result (<kbd>Shift</kbd> +{' '}
|
||||
<kbd>Enter</kbd>)
|
||||
</React.Fragment>
|
||||
}>
|
||||
<ButtonIcon type="up" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
|
||||
title={
|
||||
<React.Fragment>
|
||||
Scroll to next search result (<kbd>Enter</kbd>)
|
||||
</React.Fragment>
|
||||
}>
|
||||
<ButtonIcon type="down" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={resetSearch}
|
||||
title="Reset search">
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className={styles.LeftVRule} />
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={() => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'})}
|
||||
title={
|
||||
<React.Fragment>
|
||||
Scroll to previous search result (<kbd>Shift</kbd> +{' '}
|
||||
<kbd>Enter</kbd>)
|
||||
</React.Fragment>
|
||||
}>
|
||||
<ButtonIcon type="up" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={() => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'})}
|
||||
title={
|
||||
<React.Fragment>
|
||||
Scroll to next search result (<kbd>Enter</kbd>)
|
||||
</React.Fragment>
|
||||
}>
|
||||
<ButtonIcon type="down" />
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
disabled={!searchText}
|
||||
onClick={resetSearch}
|
||||
title="Reset search">
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
.VRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
flex: 0 0 1px;
|
||||
margin: 0 0.5rem;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
@@ -58,3 +59,23 @@
|
||||
font-size: var(--font-size-sans-large);
|
||||
color: var(--color-dim);
|
||||
}
|
||||
|
||||
.IconAndCount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-sans-normal);
|
||||
}
|
||||
|
||||
.ErrorIcon, .WarningIcon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ErrorIcon {
|
||||
color: var(--color-console-error-icon);
|
||||
}
|
||||
.WarningIcon {
|
||||
color: var(--color-console-warning-icon);
|
||||
}
|
||||
|
||||
@@ -21,18 +21,21 @@ import {
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {FixedSizeList} from 'react-window';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import Icon from '../Icon';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import ElementView from './Element';
|
||||
import Element from './Element';
|
||||
import InspectHostNodesToggle from './InspectHostNodesToggle';
|
||||
import OwnersStack from './OwnersStack';
|
||||
import SearchInput from './SearchInput';
|
||||
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
|
||||
import SelectedTreeHighlight from './SelectedTreeHighlight';
|
||||
import TreeFocusedContext from './TreeFocusedContext';
|
||||
import {useHighlightNativeElement} from '../hooks';
|
||||
import {useHighlightNativeElement, useSubscription} from '../hooks';
|
||||
|
||||
import styles from './Tree.css';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
|
||||
// Never indent more than this number of pixels (even if we have the room).
|
||||
const DEFAULT_INDENTATION_SIZE = 12;
|
||||
@@ -71,7 +74,7 @@ export default function Tree(props: Props) {
|
||||
|
||||
const [treeFocused, setTreeFocused] = useState<boolean>(false);
|
||||
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext);
|
||||
|
||||
// Make sure a newly selected element is visible in the list.
|
||||
// This is helpful for things like the owners list and search.
|
||||
@@ -301,6 +304,29 @@ export default function Tree(props: Props) {
|
||||
[store],
|
||||
);
|
||||
|
||||
const handlePreviousErrorOrWarningClick = React.useCallback(() => {
|
||||
dispatch({type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
|
||||
}, []);
|
||||
|
||||
const handleNextErrorOrWarningClick = React.useCallback(() => {
|
||||
dispatch({type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE'});
|
||||
}, []);
|
||||
|
||||
const errorsOrWarningsSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => ({
|
||||
errors: store.errorCount,
|
||||
warnings: store.warningCount,
|
||||
}),
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('mutated', callback);
|
||||
return () => store.removeListener('mutated', callback);
|
||||
},
|
||||
}),
|
||||
[store],
|
||||
);
|
||||
const {errors, warnings} = useSubscription(errorsOrWarningsSubscription);
|
||||
|
||||
return (
|
||||
<TreeFocusedContext.Provider value={treeFocused}>
|
||||
<div className={styles.Tree} ref={treeRef}>
|
||||
@@ -315,6 +341,40 @@ export default function Tree(props: Props) {
|
||||
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
|
||||
</Suspense>
|
||||
<div className={styles.VRule} />
|
||||
{showInlineWarningsAndErrors &&
|
||||
ownerID === null &&
|
||||
(errors > 0 || warnings > 0) && (
|
||||
<React.Fragment>
|
||||
{errors > 0 && (
|
||||
<div className={styles.IconAndCount}>
|
||||
<Icon className={styles.ErrorIcon} type="error" />
|
||||
{errors}
|
||||
</div>
|
||||
)}
|
||||
{warnings > 0 && (
|
||||
<div className={styles.IconAndCount}>
|
||||
<Icon className={styles.WarningIcon} type="warning" />
|
||||
{warnings}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handlePreviousErrorOrWarningClick}
|
||||
title="Scroll to previous error or warning">
|
||||
<ButtonIcon type="up" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleNextErrorOrWarningClick}
|
||||
title="Scroll to next error or warning">
|
||||
<ButtonIcon type="down" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => store.clearErrorsAndWarnings()}
|
||||
title="Clear all errors and warnings">
|
||||
<ButtonIcon type="clear" />
|
||||
</Button>
|
||||
<div className={styles.VRule} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<SettingsModalContextToggle />
|
||||
</div>
|
||||
<div
|
||||
@@ -339,7 +399,7 @@ export default function Tree(props: Props) {
|
||||
itemSize={lineHeight}
|
||||
ref={listCallbackRef}
|
||||
width={width}>
|
||||
{ElementView}
|
||||
{Element}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -93,6 +93,9 @@ type ACTION_SELECT_ELEMENT_BY_ID = {|
|
||||
type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
|
||||
type: 'SELECT_NEXT_ELEMENT_IN_TREE',
|
||||
|};
|
||||
type ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
|
||||
type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
|
||||
|};
|
||||
type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {|
|
||||
type: 'SELECT_NEXT_SIBLING_IN_TREE',
|
||||
|};
|
||||
@@ -106,6 +109,9 @@ type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
|
||||
type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
|
||||
type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
|
||||
|};
|
||||
type ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {|
|
||||
type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
|
||||
|};
|
||||
type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {|
|
||||
type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
|
||||
|};
|
||||
@@ -132,10 +138,12 @@ type Action =
|
||||
| ACTION_SELECT_ELEMENT_AT_INDEX
|
||||
| ACTION_SELECT_ELEMENT_BY_ID
|
||||
| ACTION_SELECT_NEXT_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
|
||||
| ACTION_SELECT_NEXT_SIBLING_IN_TREE
|
||||
| ACTION_SELECT_OWNER
|
||||
| ACTION_SELECT_PARENT_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
|
||||
| ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
|
||||
| ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
|
||||
| ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
|
||||
@@ -372,6 +380,83 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
|
||||
if (store.errorCount === 0 && store.warningCount === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
|
||||
|
||||
let flatIndex = 0;
|
||||
if (selectedElementIndex !== null) {
|
||||
// Resume from the current position in the list.
|
||||
// Otherwise step to the previous item, relative to the current selection.
|
||||
for (
|
||||
let i = elementIndicesWithErrorsOrWarnings.length - 1;
|
||||
i >= 0;
|
||||
i--
|
||||
) {
|
||||
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
||||
if (index >= selectedElementIndex) {
|
||||
flatIndex = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prevEntry;
|
||||
if (flatIndex === 0) {
|
||||
prevEntry =
|
||||
elementIndicesWithErrorsOrWarnings[
|
||||
elementIndicesWithErrorsOrWarnings.length - 1
|
||||
];
|
||||
selectedElementID = prevEntry.id;
|
||||
selectedElementIndex = prevEntry.index;
|
||||
} else {
|
||||
prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
|
||||
selectedElementID = prevEntry.id;
|
||||
selectedElementIndex = prevEntry.index;
|
||||
}
|
||||
|
||||
lookupIDForIndex = false;
|
||||
break;
|
||||
}
|
||||
case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
|
||||
if (store.errorCount === 0 && store.warningCount === 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const elementIndicesWithErrorsOrWarnings = store.getElementsWithErrorsAndWarnings();
|
||||
|
||||
let flatIndex = -1;
|
||||
if (selectedElementIndex !== null) {
|
||||
// Resume from the current position in the list.
|
||||
// Otherwise step to the next item, relative to the current selection.
|
||||
for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
|
||||
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
||||
if (index <= selectedElementIndex) {
|
||||
flatIndex = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nextEntry;
|
||||
if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
|
||||
nextEntry = elementIndicesWithErrorsOrWarnings[0];
|
||||
selectedElementID = nextEntry.id;
|
||||
selectedElementIndex = nextEntry.index;
|
||||
} else {
|
||||
nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
|
||||
selectedElementID = nextEntry.id;
|
||||
selectedElementIndex = nextEntry.index;
|
||||
}
|
||||
|
||||
lookupIDForIndex = false;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
@@ -776,11 +861,13 @@ function TreeContextController({
|
||||
case 'SELECT_ELEMENT_BY_ID':
|
||||
case 'SELECT_CHILD_ELEMENT_IN_TREE':
|
||||
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
||||
case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
|
||||
case 'SELECT_NEXT_SIBLING_IN_TREE':
|
||||
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
|
||||
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
|
||||
case 'SELECT_PARENT_ELEMENT_IN_TREE':
|
||||
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
||||
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
|
||||
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
|
||||
case 'SELECT_OWNER':
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
|
||||
@@ -84,6 +84,8 @@ export type InspectedElement = {|
|
||||
props: Object | null,
|
||||
state: Object | null,
|
||||
key: number | string | null,
|
||||
errors: Array<[string, number]>,
|
||||
warnings: Array<[string, number]>,
|
||||
|
||||
// List of owners
|
||||
owners: Array<Owner> | null,
|
||||
|
||||
+13
-1
@@ -16,13 +16,15 @@ export type IconType =
|
||||
| 'code'
|
||||
| 'components'
|
||||
| 'copy'
|
||||
| 'error'
|
||||
| 'flame-chart'
|
||||
| 'interactions'
|
||||
| 'profiler'
|
||||
| 'ranked-chart'
|
||||
| 'search'
|
||||
| 'settings'
|
||||
| 'store-as-global-variable';
|
||||
| 'store-as-global-variable'
|
||||
| 'warning';
|
||||
|
||||
type Props = {|
|
||||
className?: string,
|
||||
@@ -47,6 +49,9 @@ export default function Icon({className = '', type}: Props) {
|
||||
case 'copy':
|
||||
pathData = PATH_COPY;
|
||||
break;
|
||||
case 'error':
|
||||
pathData = PATH_ERROR;
|
||||
break;
|
||||
case 'flame-chart':
|
||||
pathData = PATH_FLAME_CHART;
|
||||
break;
|
||||
@@ -68,6 +73,9 @@ export default function Icon({className = '', type}: Props) {
|
||||
case 'store-as-global-variable':
|
||||
pathData = PATH_STORE_AS_GLOBAL_VARIABLE;
|
||||
break;
|
||||
case 'warning':
|
||||
pathData = PATH_WARNING;
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unsupported type "${type}" specified for Icon`);
|
||||
break;
|
||||
@@ -107,6 +115,8 @@ const PATH_COPY = `
|
||||
2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z
|
||||
`;
|
||||
|
||||
const PATH_ERROR = `M16.971 0h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-1.402 16.945l-3.554-3.521-3.518 3.568-1.418-1.418 3.507-3.566-3.586-3.472 1.418-1.417 3.581 3.458 3.539-3.583 1.431 1.431-3.535 3.568 3.566 3.522-1.431 1.43z`;
|
||||
|
||||
const PATH_FLAME_CHART = `
|
||||
M10.0650893,21.5040462 C7.14020814,20.6850349 5,18.0558698 5,14.9390244 C5,14.017627
|
||||
5,9.81707317 7.83333333,7.37804878 C7.83333333,7.37804878 7.58333333,11.199187 10,
|
||||
@@ -154,3 +164,5 @@ const PATH_STORE_AS_GLOBAL_VARIABLE = `
|
||||
1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
|
||||
8h-4v-2h4v2zm0-4h-4v-2h4v2z
|
||||
`;
|
||||
|
||||
const PATH_WARNING = `M12 1l-12 22h24l-12-22zm-1 8h2v7h-2v-7zm1 11.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z`;
|
||||
|
||||
+20
@@ -11,8 +11,10 @@ import {
|
||||
__DEBUG__,
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REMOVE_ROOT,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {utfDecodeString} from 'react-devtools-shared/src/utils';
|
||||
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
|
||||
@@ -294,6 +296,9 @@ function updateTree(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_REMOVE_ROOT: {
|
||||
throw Error('Operation REMOVE_ROOT is not supported while profiling.');
|
||||
}
|
||||
case TREE_OPERATION_REORDER_CHILDREN: {
|
||||
id = ((operations[i + 1]: any): number);
|
||||
const numChildren = ((operations[i + 2]: any): number);
|
||||
@@ -329,6 +334,21 @@ function updateTree(
|
||||
i += 3;
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
|
||||
id = operations[i + 1];
|
||||
const numErrors = operations[i + 2];
|
||||
const numWarnings = operations[i + 3];
|
||||
|
||||
i += 4;
|
||||
|
||||
if (__DEBUG__) {
|
||||
debug(
|
||||
'Warnings and Errors update',
|
||||
`fiber ${id} has ${numErrors} errors and ${numWarnings} warnings`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation ${operation}`);
|
||||
}
|
||||
|
||||
+19
@@ -19,6 +19,8 @@ export default function DebuggingSettings(_: {||}) {
|
||||
breakOnConsoleErrors,
|
||||
setAppendComponentStack,
|
||||
setBreakOnConsoleErrors,
|
||||
setShowInlineWarningsAndErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
} = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
@@ -36,6 +38,19 @@ export default function DebuggingSettings(_: {||}) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showInlineWarningsAndErrors}
|
||||
onChange={({currentTarget}) =>
|
||||
setShowInlineWarningsAndErrors(currentTarget.checked)
|
||||
}
|
||||
/>{' '}
|
||||
Show inline warnings and errors.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
@@ -48,6 +63,10 @@ export default function DebuggingSettings(_: {||}) {
|
||||
Break on warnings
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.ConsoleAPIWarning}>
|
||||
These settings require DevTools to override native console APIs.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+41
-1
@@ -21,6 +21,7 @@ import {
|
||||
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
||||
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {useLocalStorage} from '../hooks';
|
||||
import {BridgeContext} from '../context';
|
||||
@@ -44,6 +45,9 @@ type Context = {|
|
||||
breakOnConsoleErrors: boolean,
|
||||
setBreakOnConsoleErrors: (value: boolean) => void,
|
||||
|
||||
showInlineWarningsAndErrors: boolean,
|
||||
setShowInlineWarningsAndErrors: (value: boolean) => void,
|
||||
|
||||
theme: Theme,
|
||||
setTheme(value: Theme): void,
|
||||
|
||||
@@ -90,6 +94,13 @@ function SettingsContextController({
|
||||
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
||||
false,
|
||||
);
|
||||
const [
|
||||
showInlineWarningsAndErrors,
|
||||
setShowInlineWarningsAndErrors,
|
||||
] = useLocalStorage<boolean>(
|
||||
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
||||
true,
|
||||
);
|
||||
const [
|
||||
traceUpdatesEnabled,
|
||||
setTraceUpdatesEnabled,
|
||||
@@ -147,8 +158,14 @@ function SettingsContextController({
|
||||
bridge.send('updateConsolePatchSettings', {
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
});
|
||||
}, [bridge, appendComponentStack, breakOnConsoleErrors]);
|
||||
}, [
|
||||
bridge,
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
|
||||
@@ -168,6 +185,8 @@ function SettingsContextController({
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
setTraceUpdatesEnabled,
|
||||
setShowInlineWarningsAndErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
}),
|
||||
@@ -180,6 +199,8 @@ function SettingsContextController({
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
setTraceUpdatesEnabled,
|
||||
setShowInlineWarningsAndErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
],
|
||||
@@ -324,6 +345,25 @@ export function updateThemeVariables(
|
||||
'color-component-badge-count-inverted',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(theme, 'color-console-error-badge-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-error-background', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-error-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-error-icon', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-error-text', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-console-warning-badge-text',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-console-warning-background',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(theme, 'color-console-warning-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-warning-icon', documentElements);
|
||||
updateStyleHelper(theme, 'color-console-warning-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-context-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-context-background', documentElements);
|
||||
updateStyleHelper(theme, 'color-context-background-hover', documentElements);
|
||||
updateStyleHelper(
|
||||
|
||||
@@ -42,4 +42,4 @@
|
||||
padding: 0.5rem;
|
||||
flex: 0 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,8 @@
|
||||
background-color: var(--color-toggle-text);
|
||||
}
|
||||
|
||||
.ReleaseNotes {
|
||||
.ReleaseNotes,
|
||||
.ConsoleAPIWarning {
|
||||
width: 100%;
|
||||
background-color: var(--color-background-hover);
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
--light-color-component-badge-background-inverted: rgba(255, 255, 255, 0.25);
|
||||
--light-color-component-badge-count: #777d88;
|
||||
--light-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
|
||||
--light-color-console-error-badge-text: #ffffff;
|
||||
--light-color-console-error-background: #fff0f0;
|
||||
--light-color-console-error-border: #ffd6d6;
|
||||
--light-color-console-error-icon: #eb3941;
|
||||
--light-color-console-error-text: #fe2e31;
|
||||
--light-color-console-warning-badge-text: #000000;
|
||||
--light-color-console-warning-background: #fffbe5;
|
||||
--light-color-console-warning-border: #fff5c1;
|
||||
--light-color-console-warning-icon: #f4bd00;
|
||||
--light-color-console-warning-text: #64460c;
|
||||
--light-color-context-background: rgba(0,0,0,.9);
|
||||
--light-color-context-background-hover: rgba(255, 255, 255, 0.1);
|
||||
--light-color-context-background-selected: #178fb9;
|
||||
@@ -121,6 +131,16 @@
|
||||
--dark-color-component-badge-background-inverted: rgba(0, 0, 0, 0.25);
|
||||
--dark-color-component-badge-count: #8f949d;
|
||||
--dark-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
|
||||
--dark-color-console-error-badge-text: #000000;
|
||||
--dark-color-console-error-background: #290000;
|
||||
--dark-color-console-error-border: #5c0000;
|
||||
--dark-color-console-error-icon: #eb3941;
|
||||
--dark-color-console-error-text: #fc7f7f;
|
||||
--dark-color-console-warning-badge-text: #000000;
|
||||
--dark-color-console-warning-background: #332b00;
|
||||
--dark-color-console-warning-border: #665500;
|
||||
--dark-color-console-warning-icon: #f4bd00;
|
||||
--dark-color-console-warning-text: #f5f2ed;
|
||||
--dark-color-context-background: rgba(255,255,255,.9);
|
||||
--dark-color-context-background-hover: rgba(0, 136, 250, 0.1);
|
||||
--dark-color-context-background-selected: #0088fa;
|
||||
|
||||
+3
@@ -182,6 +182,8 @@ export function installHook(target: any): DevToolsHook | null {
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
|
||||
const breakOnConsoleErrors =
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
|
||||
const showInlineWarningsAndErrors =
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ === true;
|
||||
|
||||
// The installHook() function is injected by being stringified in the browser,
|
||||
// so imports outside of this function do not get included.
|
||||
@@ -195,6 +197,7 @@ export function installHook(target: any): DevToolsHook | null {
|
||||
patchConsole({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
+39
@@ -26,7 +26,9 @@ import {REACT_SUSPENSE_LIST_TYPE as SuspenseList} from 'shared/ReactSymbols';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_REMOVE_ROOT,
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from './constants';
|
||||
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
||||
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
||||
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
||||
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
||||
} from './constants';
|
||||
import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
|
||||
import {
|
||||
@@ -204,6 +207,12 @@ export function printOperationsArray(operations: Array<number>) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_REMOVE_ROOT: {
|
||||
i += 1;
|
||||
|
||||
logs.push(`Remove root ${rootID}`);
|
||||
break;
|
||||
}
|
||||
case TREE_OPERATION_REORDER_CHILDREN: {
|
||||
const id = ((operations[i + 1]: any): number);
|
||||
const numChildren = ((operations[i + 2]: any): number);
|
||||
@@ -220,6 +229,17 @@ export function printOperationsArray(operations: Array<number>) {
|
||||
// The profiler UI uses them lazily in order to generate the tree.
|
||||
i += 3;
|
||||
break;
|
||||
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
|
||||
const id = operations[i + 1];
|
||||
const numErrors = operations[i + 2];
|
||||
const numWarnings = operations[i + 3];
|
||||
|
||||
i += 4;
|
||||
|
||||
logs.push(
|
||||
`Node ${id} has ${numErrors} errors and ${numWarnings} warnings`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation ${operation}`);
|
||||
}
|
||||
@@ -293,6 +313,25 @@ export function setBreakOnConsoleErrors(value: boolean): void {
|
||||
);
|
||||
}
|
||||
|
||||
export function getShowInlineWarningsAndErrors(): boolean {
|
||||
try {
|
||||
const raw = localStorageGetItem(
|
||||
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
||||
);
|
||||
if (raw != null) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
} catch (error) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setShowInlineWarningsAndErrors(value: boolean): void {
|
||||
localStorageSetItem(
|
||||
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
||||
JSON.stringify(value),
|
||||
);
|
||||
}
|
||||
|
||||
export function separateDisplayNameAndHOCs(
|
||||
displayName: string | null,
|
||||
type: ElementType,
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/** @flow */
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useEffect, useRef, useState} from 'react';
|
||||
|
||||
function WarnDuringRender({children = null}) {
|
||||
console.warn('This warning fires during every render');
|
||||
return children;
|
||||
}
|
||||
|
||||
function WarnOnMount({children = null}) {
|
||||
useEffect(() => {
|
||||
console.warn('This warning fires on initial mount only');
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function WarnOnUpdate({children = null}) {
|
||||
const didMountRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (didMountRef.current) {
|
||||
console.warn('This warning fires on every update');
|
||||
} else {
|
||||
didMountRef.current = true;
|
||||
}
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
function WarnOnUnmount({children = null}) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.warn('This warning fires on unmount');
|
||||
};
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorDuringRender({children = null}) {
|
||||
console.error('This error fires during every render');
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorOnMount({children = null}) {
|
||||
useEffect(() => {
|
||||
console.error('This error fires on initial mount only');
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorOnUpdate({children = null}) {
|
||||
const didMountRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (didMountRef.current) {
|
||||
console.error('This error fires on every update');
|
||||
} else {
|
||||
didMountRef.current = true;
|
||||
}
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorOnUnmount({children = null}) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.error('This error fires on unmount');
|
||||
};
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorAndWarningDuringRender({children = null}) {
|
||||
console.warn('This warning fires during every render');
|
||||
console.error('This error fires during every render');
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorAndWarningOnMount({children = null}) {
|
||||
useEffect(() => {
|
||||
console.warn('This warning fires on initial mount only');
|
||||
console.error('This error fires on initial mount only');
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorAndWarningOnUpdate({children = null}) {
|
||||
const didMountRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (didMountRef.current) {
|
||||
console.warn('This warning fires on every update');
|
||||
console.error('This error fires on every update');
|
||||
} else {
|
||||
didMountRef.current = true;
|
||||
}
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorAndWarningOnUnmount({children = null}) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.warn('This warning fires on unmount');
|
||||
console.error('This error fires on unmount');
|
||||
};
|
||||
}, []);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ReallyLongErrorMessageThatWillCauseTextToBeTruncated({
|
||||
children = null,
|
||||
}) {
|
||||
console.error(
|
||||
'This error is a really long error message that should cause the text to be truncated in DevTools',
|
||||
);
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorWithMultipleArgs({children = null}) {
|
||||
console.error('This error', 'passes console', 4, 'arguments');
|
||||
return children;
|
||||
}
|
||||
|
||||
function ErrorWithStringSubstitutions({children = null}) {
|
||||
console.error('This error uses "%s" substitutions', 'string');
|
||||
return children;
|
||||
}
|
||||
|
||||
function ReactErrorOnHostComponent({children = null}) {
|
||||
return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
|
||||
}
|
||||
|
||||
function DuplicateWarningsAndErrors({children = null}) {
|
||||
console.warn('this warning is logged twice per render');
|
||||
console.warn('this warning is logged twice per render');
|
||||
console.error('this error is logged twice per render');
|
||||
console.error('this error is logged twice per render');
|
||||
return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
|
||||
}
|
||||
|
||||
function MultipleWarningsAndErrors({children = null}) {
|
||||
console.warn('this is the first warning logged');
|
||||
console.warn('this is the second warning logged');
|
||||
console.error('this is the first error logged');
|
||||
console.error('this is the second error logged');
|
||||
return <div data-camelCasedAttribute="should-lower-case">{children}</div>;
|
||||
}
|
||||
|
||||
function ComponentWithMissingKey({children}) {
|
||||
return [<div />];
|
||||
}
|
||||
|
||||
export default function ErrorsAndWarnings() {
|
||||
const [count, setCount] = useState(0);
|
||||
const handleClick = () => setCount(count + 1);
|
||||
return (
|
||||
<Fragment>
|
||||
<h1>Inline warnings</h1>
|
||||
<button onClick={handleClick}>Update {count > 0 ? count : ''}</button>
|
||||
<ComponentWithMissingKey />
|
||||
<WarnDuringRender />
|
||||
<WarnOnMount />
|
||||
<WarnOnUpdate />
|
||||
{count === 0 ? <WarnOnUnmount /> : null}
|
||||
{count === 0 ? <WarnOnMount /> : null}
|
||||
<ErrorDuringRender />
|
||||
<ErrorOnMount />
|
||||
<ErrorOnUpdate />
|
||||
{count === 0 ? <ErrorOnUnmount /> : null}
|
||||
<ErrorAndWarningDuringRender />
|
||||
<ErrorAndWarningOnMount />
|
||||
<ErrorAndWarningOnUpdate />
|
||||
{count === 0 ? <ErrorAndWarningOnUnmount /> : null}
|
||||
<ErrorWithMultipleArgs />
|
||||
<ErrorWithStringSubstitutions />
|
||||
<ReactErrorOnHostComponent />
|
||||
<ReallyLongErrorMessageThatWillCauseTextToBeTruncated />
|
||||
<DuplicateWarningsAndErrors />
|
||||
<MultipleWarningsAndErrors />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import Iframe from './Iframe';
|
||||
import EditableProps from './EditableProps';
|
||||
import ElementTypes from './ElementTypes';
|
||||
import Hydration from './Hydration';
|
||||
import InlineWarnings from './InlineWarnings';
|
||||
import InspectableElements from './InspectableElements';
|
||||
import InteractionTracing from './InteractionTracing';
|
||||
import PriorityLevels from './PriorityLevels';
|
||||
@@ -53,6 +54,7 @@ function mountTestApp() {
|
||||
mountHelper(Hydration);
|
||||
mountHelper(ElementTypes);
|
||||
mountHelper(EditableProps);
|
||||
mountHelper(InlineWarnings);
|
||||
mountHelper(PriorityLevels);
|
||||
mountHelper(ReactNativeWeb);
|
||||
mountHelper(Toggle);
|
||||
|
||||
@@ -141,12 +141,41 @@ While profiling is in progress, we send an extra operation any time a fiber is a
|
||||
For example, updating the base duration for a fiber with an id of 1:
|
||||
```js
|
||||
[
|
||||
4, // update tree base duration operation
|
||||
4, // tree base duration operation
|
||||
1, // fiber id
|
||||
32, // new tree base duration value
|
||||
]
|
||||
```
|
||||
|
||||
#### Updating errors and warnings on a Fiber
|
||||
|
||||
We record calls to `console.warn` and `console.error` in the backend.
|
||||
Periodically we notify the frontend that the number of recorded calls got updated.
|
||||
We only send the serialized messages as part of the `inspectElement` event.
|
||||
|
||||
|
||||
```js
|
||||
[
|
||||
5, // update error/warning counts operation
|
||||
4, // fiber id
|
||||
0, // number of calls to console.error from that fiber
|
||||
3, // number of calls to console.warn from that fiber
|
||||
]
|
||||
```
|
||||
|
||||
#### Removing a root
|
||||
|
||||
Special case of unmounting an entire root (include its decsendants). This specialized message replaces what would otherwise be a series of remove-node operations. It is currently only used in one case: updating component filters. The primary motivation for this is actually to preserve fiber ids for components that are re-added to the tree after the updated filters have been applied. This preserves mappings between the Fiber (id) and things like error and warning logs.
|
||||
|
||||
```js
|
||||
[
|
||||
6, // remove root operation
|
||||
]
|
||||
```
|
||||
|
||||
This operation has no additional payload because renderer and root ids are already sent at the beginning of every operations payload.
|
||||
|
||||
## Reconstructing the tree
|
||||
|
||||
The frontend stores its information about the tree in a map of id to objects with the following keys:
|
||||
@@ -268,4 +297,4 @@ Once profiling is finished, the frontend requests profiling data from the backen
|
||||
|
||||
### Importing/exporting data
|
||||
|
||||
Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
|
||||
Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as a single JSON object), enabling profiling sessions to be shared between users.
|
||||
|
||||
@@ -59,6 +59,9 @@ module.exports = Object.assign({}, baseConfig, {
|
||||
require.resolve(
|
||||
'../../packages/react-devtools-shared/src/__tests__/storeSerializer.js'
|
||||
),
|
||||
require.resolve(
|
||||
'../../packages/react-devtools-shared/src/__tests__/treeContextStateSerializer.js'
|
||||
),
|
||||
],
|
||||
setupFiles: [
|
||||
...baseConfig.setupFiles,
|
||||
|
||||
Reference in New Issue
Block a user