From ef38390666ee9c3eee52e1bfe36032b98232c989 Mon Sep 17 00:00:00 2001 From: Jirat Ki Date: Mon, 9 Jan 2017 22:26:01 +0700 Subject: [PATCH] Add component stack to invalid element type warning (#8495) * Show Source Error Addemden if __source available * Add Parent Stack on invalid element type * refactor to use normalizeCodeLocInfo * Remove ( ) from addendum --- .../classic/element/ReactElementValidator.js | 25 +++++- .../__tests__/ReactElementValidator-test.js | 9 +- .../ReactJSXElementValidator-test.js | 83 ++++++++++++++----- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index e7ad9d72a7..6613aeeb61 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -38,6 +38,20 @@ function getDeclarationErrorAddendum() { return ''; } +function getSourceInfoErrorAddendum(elementProps) { + if ( + elementProps !== null && + elementProps !== undefined && + elementProps.__source !== undefined + ) { + var source = elementProps.__source; + var fileName = source.fileName.replace(/^.*[\\\/]/, ''); + var lineNumber = source.lineNumber; + return ' Check your code at ' + fileName + ':' + lineNumber + '.'; + } + return ''; +} + /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between @@ -202,7 +216,16 @@ var ReactElementValidator = { ' You likely forgot to export your component from the file ' + 'it\'s defined in.'; } - info += getDeclarationErrorAddendum(); + + var sourceInfo = getSourceInfoErrorAddendum(props); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + + info += ReactComponentTreeHook.getCurrentStackAddendum(); + warning( false, 'React.createElement: type is invalid -- expected a string (for ' + diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 17367f0439..f2e403c5a4 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -20,7 +20,7 @@ var ReactTestUtils; describe('ReactElementValidator', () => { function normalizeCodeLocInfo(str) { - return str.replace(/\(at .+?:\d+\)/g, '(at **)'); + return str && str.replace(/at .+?:\d+/g, 'at **'); } var ComponentClass; @@ -334,7 +334,8 @@ describe('ReactElementValidator', () => { expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + - 'components) but got: null. Check the render method of `ParentComp`.' + 'components) but got: null. Check the render method of `ParentComp`.' + + '\n in ParentComp' ); }); @@ -526,11 +527,11 @@ describe('ReactElementValidator', () => { var Foo = undefined; void {[
]}; expect(console.error.calls.count()).toBe(1); - expect(console.error.calls.argsFor(0)[0]).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + 'components) but got: undefined. You likely forgot to export your ' + - 'component from the file it\'s defined in.' + 'component from the file it\'s defined in. Check your code at **.' ); }); diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 00f3650217..5bb44c6943 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -15,9 +15,14 @@ // of dynamic errors when using JSX with Flow. var React; +var ReactDOM; var ReactTestUtils; describe('ReactJSXElementValidator', () => { + function normalizeCodeLocInfo(str) { + return str && str.replace(/at .+?:\d+/g, 'at **'); + } + var Component; var RequiredPropComponent; @@ -25,6 +30,7 @@ describe('ReactJSXElementValidator', () => { jest.resetModuleRegistry(); React = require('React'); + ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); Component = class extends React.Component { @@ -195,9 +201,7 @@ describe('ReactJSXElementValidator', () => { } } ReactTestUtils.renderIntoDocument(); - expect( - console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)') - ).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Failed prop type: ' + 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + 'expected `string`.\n' + @@ -206,6 +210,43 @@ describe('ReactJSXElementValidator', () => { ); }); + it('should update component stack after receiving next element', () => { + spyOn(console, 'error'); + function MyComp() { + return null; + } + MyComp.propTypes = { + color: React.PropTypes.string, + }; + function MiddleComp(props) { + return ; + } + function ParentComp(props) { + if (props.warn) { + // This element has a source thanks to JSX. + return ; + } + // This element has no source. + return React.createElement(MiddleComp, {color: 'blue'}); + } + + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + + expect(console.error.calls.count()).toBe(1); + // The warning should have the full stack with line numbers. + // If it doesn't, it means we're using information from the old element. + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( + 'Warning: Failed prop type: ' + + 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + + 'expected `string`.\n' + + ' in MyComp (at **)\n' + + ' in MiddleComp (at **)\n' + + ' in ParentComp (at **)' + ); + }); + it('gives a helpful error when passing null, undefined, or boolean', () => { var Undefined = undefined; var Null = null; @@ -218,26 +259,30 @@ describe('ReactJSXElementValidator', () => { void ; void ; expect(console.error.calls.count()).toBe(4); - expect(console.error.calls.argsFor(0)[0]).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + 'components) but got: undefined. You likely forgot to export your ' + - 'component from the file it\'s defined in.' + 'component from the file it\'s defined in. ' + + 'Check your code at **.' ); - expect(console.error.calls.argsFor(1)[0]).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + - 'components) but got: null.' + 'components) but got: null. ' + + 'Check your code at **.' ); - expect(console.error.calls.argsFor(2)[0]).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(2)[0])).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + - 'components) but got: boolean.' + 'components) but got: boolean. ' + + 'Check your code at **.' ); - expect(console.error.calls.argsFor(3)[0]).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(3)[0])).toBe( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + - 'components) but got: number.' + 'components) but got: number. ' + + 'Check your code at **.' ); void
; expect(console.error.calls.count()).toBe(4); @@ -251,9 +296,7 @@ describe('ReactJSXElementValidator', () => { ReactTestUtils.renderIntoDocument(); expect(console.error.calls.count()).toBe(1); - expect( - console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)') - ).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Failed prop type: The prop `prop` is marked as required in ' + '`RequiredPropComponent`, but its value is `null`.\n' + ' in RequiredPropComponent (at **)' @@ -266,9 +309,7 @@ describe('ReactJSXElementValidator', () => { ReactTestUtils.renderIntoDocument(); expect(console.error.calls.count()).toBe(1); - expect( - console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)') - ).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Failed prop type: The prop `prop` is marked as required in ' + '`RequiredPropComponent`, but its value is `null`.\n' + ' in RequiredPropComponent (at **)' @@ -282,18 +323,14 @@ describe('ReactJSXElementValidator', () => { ReactTestUtils.renderIntoDocument(); expect(console.error.calls.count()).toBe(2); - expect( - console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)') - ).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Failed prop type: ' + 'The prop `prop` is marked as required in `RequiredPropComponent`, but ' + 'its value is `undefined`.\n' + ' in RequiredPropComponent (at **)' ); - expect( - console.error.calls.argsFor(1)[0].replace(/\(at .+?:\d+\)/g, '(at **)') - ).toBe( + expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( 'Warning: Failed prop type: ' + 'Invalid prop `prop` of type `number` supplied to ' + '`RequiredPropComponent`, expected `string`.\n' +