Skip to content

Commit

Permalink
Add component stack to invalid element type warning (#8495)
Browse files Browse the repository at this point in the history
* Show Source Error Addemden if __source available

* Add Parent Stack on invalid element type

* refactor to use normalizeCodeLocInfo

* Remove ( ) from addendum
  • Loading branch information
n3tr authored and Brian Vaughn committed Mar 28, 2017
1 parent 17434d7 commit ef38390
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 28 deletions.
25 changes: 24 additions & 1 deletion src/isomorphic/classic/element/ReactElementValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
);
});

Expand Down Expand Up @@ -526,11 +527,11 @@ describe('ReactElementValidator', () => {
var Foo = undefined;
void <Foo>{[<div />]}</Foo>;
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 **.'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@
// 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;

beforeEach(() => {
jest.resetModuleRegistry();

React = require('React');
ReactDOM = require('ReactDOM');
ReactTestUtils = require('ReactTestUtils');

Component = class extends React.Component {
Expand Down Expand Up @@ -195,13 +201,48 @@ describe('ReactJSXElementValidator', () => {
}
}
ReactTestUtils.renderIntoDocument(<ParentComp />);
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' +
' in MyComp (at **)\n' +
' in ParentComp (at **)'
);
});

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 <MyComp color={props.color} />;
}
function ParentComp(props) {
if (props.warn) {
// This element has a source thanks to JSX.
return <MiddleComp color={42} />;
}
// This element has no source.
return React.createElement(MiddleComp, {color: 'blue'});
}

var container = document.createElement('div');
ReactDOM.render(<ParentComp warn={false} />, container);
ReactDOM.render(<ParentComp warn={true} />, 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 **)'
);
});
Expand All @@ -218,26 +259,30 @@ describe('ReactJSXElementValidator', () => {
void <True />;
void <Num />;
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 <Div />;
expect(console.error.calls.count()).toBe(4);
Expand All @@ -251,9 +296,7 @@ describe('ReactJSXElementValidator', () => {
ReactTestUtils.renderIntoDocument(<RequiredPropComponent />);

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 **)'
Expand All @@ -266,9 +309,7 @@ describe('ReactJSXElementValidator', () => {
ReactTestUtils.renderIntoDocument(<RequiredPropComponent prop={null} />);

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 **)'
Expand All @@ -282,18 +323,14 @@ describe('ReactJSXElementValidator', () => {
ReactTestUtils.renderIntoDocument(<RequiredPropComponent prop={42} />);

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' +
Expand Down

0 comments on commit ef38390

Please sign in to comment.