diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 367e55ed28d4d..af0111e4eb9c8 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -21,6 +21,7 @@ function createReactEventComponent(targetEventTypes, handleEvent) { return { $$typeof: Symbol.for('react.event_component'), + displayName: 'TestEventComponent', props: null, responder: testEventResponder, }; diff --git a/packages/react-events/src/Drag.js b/packages/react-events/src/Drag.js index cc741e29b5b32..54498760ff305 100644 --- a/packages/react-events/src/Drag.js +++ b/packages/react-events/src/Drag.js @@ -211,6 +211,7 @@ const DragResponder = { export default { $$typeof: REACT_EVENT_COMPONENT_TYPE, + displayName: 'Drag', props: null, responder: DragResponder, }; diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js index c574f1e3a1f9b..e56379f8871dd 100644 --- a/packages/react-events/src/Focus.js +++ b/packages/react-events/src/Focus.js @@ -122,6 +122,7 @@ const FocusResponder = { export default { $$typeof: REACT_EVENT_COMPONENT_TYPE, + displayName: 'Focus', props: null, responder: FocusResponder, }; diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js index 75992d246aa17..226430b838d09 100644 --- a/packages/react-events/src/Hover.js +++ b/packages/react-events/src/Hover.js @@ -204,6 +204,7 @@ const HoverResponder = { export default { $$typeof: REACT_EVENT_COMPONENT_TYPE, + displayName: 'Hover', props: null, responder: HoverResponder, }; diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js index 7611d8660c185..b79c38758b9a8 100644 --- a/packages/react-events/src/Press.js +++ b/packages/react-events/src/Press.js @@ -395,6 +395,7 @@ const PressResponder = { export default { $$typeof: REACT_EVENT_COMPONENT_TYPE, + displayName: 'Press', props: null, responder: PressResponder, }; diff --git a/packages/react-events/src/Swipe.js b/packages/react-events/src/Swipe.js index c67b502316e74..85df2cca3f10e 100644 --- a/packages/react-events/src/Swipe.js +++ b/packages/react-events/src/Swipe.js @@ -239,6 +239,7 @@ const SwipeResponder = { export default { $$typeof: REACT_EVENT_COMPONENT_TYPE, + displayName: 'Swipe', props: null, responder: SwipeResponder, }; diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js index 1505c1f3033a4..af3b93239e480 100644 --- a/packages/react-events/src/__tests__/Press-test.internal.js +++ b/packages/react-events/src/__tests__/Press-test.internal.js @@ -394,4 +394,8 @@ describe('Event responder: Press', () => { expect(events).toEqual(['keydown', 'inner: onPress', 'outer: onPress']); }); }); + + it('expect displayName to show up for event component', () => { + expect(Press.displayName).toBe('Press'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js index 25162397b0490..73dcc8d4a01e5 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js @@ -29,6 +29,7 @@ const noOpResponder = { function createReactEventComponent() { return { $$typeof: ReactSymbols.REACT_EVENT_COMPONENT_TYPE, + displayName: 'TestEventComponent', props: null, responder: noOpResponder, }; @@ -37,6 +38,7 @@ function createReactEventComponent() { function createReactEventTarget() { return { $$typeof: ReactSymbols.REACT_EVENT_TARGET_TYPE, + displayName: 'TestEventTarget', type: Symbol.for('react.event_target.test'), }; } @@ -392,6 +394,54 @@ describe('ReactFiberEvents', () => { 'Warning: validateDOMNesting: React event targets must not have event components as children.', ); }); + + it('should error with a component stack contains the names of the event components and event targets', () => { + let componentStackMessage; + + function ErrorComponent() { + throw new Error('Failed!'); + } + + const Test = () => ( + + + + + + + + ); + + class Wrapper extends React.Component { + state = { + error: null, + }; + + componentDidCatch(error, errMessage) { + componentStackMessage = errMessage.componentStack; + this.setState({ + error, + }); + } + + render() { + if (this.state.error) { + return null; + } + return ; + } + } + + ReactNoop.render(); + expect(Scheduler).toFlushWithoutYielding(); + + expect(componentStackMessage.includes('ErrorComponent')).toBe(true); + expect(componentStackMessage.includes('span')).toBe(true); + expect(componentStackMessage.includes('TestEventTarget')).toBe(true); + expect(componentStackMessage.includes('TestEventComponent')).toBe(true); + expect(componentStackMessage.includes('Test')).toBe(true); + expect(componentStackMessage.includes('Wrapper')).toBe(true); + }); }); describe('TestRenderer', () => { @@ -570,7 +620,7 @@ describe('ReactFiberEvents', () => { error: null, }; - componentDidCatch(error) { + componentDidCatch(error, errStack) { this.setState({ error, }); @@ -734,6 +784,55 @@ describe('ReactFiberEvents', () => { 'Warning: validateDOMNesting: React event targets must not have event components as children.', ); }); + + it('should error with a component stack contains the names of the event components and event targets', () => { + let componentStackMessage; + + function ErrorComponent() { + throw new Error('Failed!'); + } + + const Test = () => ( + + + + + + + + ); + + class Wrapper extends React.Component { + state = { + error: null, + }; + + componentDidCatch(error, errMessage) { + componentStackMessage = errMessage.componentStack; + this.setState({ + error, + }); + } + + render() { + if (this.state.error) { + return null; + } + return ; + } + } + + const root = ReactTestRenderer.create(null); + root.update(); + expect(Scheduler).toFlushWithoutYielding(); + + expect(componentStackMessage.includes('ErrorComponent')).toBe(true); + expect(componentStackMessage.includes('span')).toBe(true); + expect(componentStackMessage.includes('TestEventTarget')).toBe(true); + expect(componentStackMessage.includes('TestEventComponent')).toBe(true); + expect(componentStackMessage.includes('Test')).toBe(true); + expect(componentStackMessage.includes('Wrapper')).toBe(true); + }); }); describe('ReactDOM', () => { @@ -1053,6 +1152,54 @@ describe('ReactFiberEvents', () => { 'Warning: validateDOMNesting: React event targets must not have event components as children.', ); }); + + it('should error with a component stack contains the names of the event components and event targets', () => { + let componentStackMessage; + + function ErrorComponent() { + throw new Error('Failed!'); + } + + const Test = () => ( + + + + + + + + ); + + class Wrapper extends React.Component { + state = { + error: null, + }; + + componentDidCatch(error, errMessage) { + componentStackMessage = errMessage.componentStack; + this.setState({ + error, + }); + } + + render() { + if (this.state.error) { + return null; + } + return ; + } + } + + const container = document.createElement('div'); + ReactDOM.render(, container); + + expect(componentStackMessage.includes('ErrorComponent')).toBe(true); + expect(componentStackMessage.includes('span')).toBe(true); + expect(componentStackMessage.includes('TestEventTarget')).toBe(true); + expect(componentStackMessage.includes('TestEventComponent')).toBe(true); + expect(componentStackMessage.includes('Test')).toBe(true); + expect(componentStackMessage.includes('Wrapper')).toBe(true); + }); }); describe('ReactDOMServer', () => { diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index cedd17d170a2a..52ce27f9dfb10 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -93,11 +93,13 @@ export type ReactEventResponder = { export type ReactEventComponent = {| $$typeof: Symbol | number, + displayName?: string, props: null | Object, responder: ReactEventResponder, |}; export type ReactEventTarget = {| $$typeof: Symbol | number, + displayName?: string, type: Symbol | number, |}; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index cbea431c8f7a8..cfa07d91e1d13 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -22,8 +22,12 @@ import { REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_TYPE, REACT_LAZY_TYPE, + REACT_EVENT_COMPONENT_TYPE, + REACT_EVENT_TARGET_TYPE, + REACT_EVENT_TARGET_TOUCH_HIT, } from 'shared/ReactSymbols'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; +import type {ReactEventComponent, ReactEventTarget} from 'shared/ReactTypes'; function getWrappedName( outerType: mixed, @@ -87,6 +91,25 @@ function getComponentName(type: mixed): string | null { if (resolvedThenable) { return getComponentName(resolvedThenable); } + break; + } + case REACT_EVENT_COMPONENT_TYPE: { + const eventComponent = ((type: any): ReactEventComponent); + const displayName = eventComponent.displayName; + if (displayName !== undefined) { + return displayName; + } + break; + } + case REACT_EVENT_TARGET_TYPE: { + const eventTarget = ((type: any): ReactEventTarget); + if (eventTarget.type === REACT_EVENT_TARGET_TOUCH_HIT) { + return 'TouchHitTarget'; + } + const displayName = eventTarget.displayName; + if (displayName !== undefined) { + return displayName; + } } } }