From cea2c387336e80f308dca106a8b2ae17d248da7b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 18 Dec 2014 16:13:29 -0800 Subject: [PATCH] Separate createElement and JSX tests This essentially copies all classic element creation tests to the modern JSX tests. The classic tests doesn't use JSX and modern tests do. The idea is that the JSX tests can start dropping dynamic checks once we have Flow support for those features. JSX won't be necessary for dropping dynamic checks. Plain object will also work. Flow will also not be necessary for JSX. However, the tests should test for the suggested combination (JSX + Flow). This also moves some misplaced tests to ReactDOM and Validator. Note that the modern tests uses ES6 classes. I will add separate tests for those. However, these are not guaranteed to have .displayName so all our error messages should check .name if available instead. This should be better abstracted but I just adhoc fix this for now. --- src/browser/__tests__/ReactDOM-test.js | 6 + src/classic/element/ReactElementValidator.js | 27 +- .../element/__tests__/ReactElement-test.js | 231 +++----------- .../__tests__/ReactElementValidator-test.js | 208 ++++++++++++- src/core/ReactCompositeComponent.js | 8 +- .../element/__tests__/ReactJSXElement-test.js | 192 ++++++++++++ .../ReactJSXElementValidator-test.js | 293 ++++++++++++++++++ 7 files changed, 750 insertions(+), 215 deletions(-) create mode 100644 src/modern/element/__tests__/ReactJSXElement-test.js create mode 100644 src/modern/element/__tests__/ReactJSXElementValidator-test.js diff --git a/src/browser/__tests__/ReactDOM-test.js b/src/browser/__tests__/ReactDOM-test.js index 68405b68f6..24fd941f67 100644 --- a/src/browser/__tests__/ReactDOM-test.js +++ b/src/browser/__tests__/ReactDOM-test.js @@ -47,6 +47,12 @@ describe('ReactDOM', function() { }); */ + it("allows a DOM element to be used with a string", function() { + var element = React.createElement('div', { className: 'foo' }); + var instance = ReactTestUtils.renderIntoDocument(element); + expect(instance.getDOMNode().tagName).toBe('DIV'); + }); + it("should allow children to be passed as an argument", function() { var argDiv = ReactTestUtils.renderIntoDocument( div(null, 'child') diff --git a/src/classic/element/ReactElementValidator.js b/src/classic/element/ReactElementValidator.js index 55624f8e89..7c262243c8 100644 --- a/src/classic/element/ReactElementValidator.js +++ b/src/classic/element/ReactElementValidator.js @@ -41,6 +41,24 @@ var loggedTypeFailures = {}; var NUMERIC_PROPERTY_REGEX = /^\d+$/; +/** + * Gets the instance's name for use in warnings. + * + * @internal + * @return {?string} Display name or undefined + */ +function getName(instance) { + var publicInstance = instance && instance.getPublicInstance(); + if (!publicInstance) { + return undefined; + } + var constructor = publicInstance.constructor; + if (!constructor) { + return undefined; + } + return constructor.displayName || constructor.name || undefined; +} + /** * Gets the current owner's displayName for use in warnings. * @@ -50,7 +68,7 @@ var NUMERIC_PROPERTY_REGEX = /^\d+$/; function getCurrentOwnerDisplayName() { var current = ReactCurrentOwner.current; return ( - current && current.getPublicInstance().constructor.displayName || undefined + current && getName(current) || undefined ); } @@ -110,7 +128,7 @@ function validatePropertyKey(name, element, parentType) { */ function warnAndMonitorForKeyUse(warningID, message, element, parentType) { var ownerName = getCurrentOwnerDisplayName(); - var parentName = parentType.displayName; + var parentName = parentType.displayName || parentType.name; var useName = ownerName || parentName; var memoizer = ownerHasKeyUseWarning[warningID]; @@ -131,8 +149,7 @@ function warnAndMonitorForKeyUse(warningID, message, element, parentType) { element._owner && element._owner !== ReactCurrentOwner.current) { // Name of the component that originally created this child. - childOwnerName = - element._owner.getPublicInstance().constructor.displayName; + childOwnerName = getName(element._owner); message += ` It was passed a child from ${childOwnerName}.`; } @@ -262,7 +279,7 @@ var ReactElementValidator = { } if (type) { - var name = type.displayName; + var name = type.displayName || type.name; if (type.propTypes) { checkPropTypes( name, diff --git a/src/classic/element/__tests__/ReactElement-test.js b/src/classic/element/__tests__/ReactElement-test.js index 0f64f24d8b..633fb56ef7 100644 --- a/src/classic/element/__tests__/ReactElement-test.js +++ b/src/classic/element/__tests__/ReactElement-test.js @@ -11,10 +11,12 @@ "use strict"; +// NOTE: We're explicitly not using JSX in this file. This is intended to test +// classic JS without JSX. + var mocks; var React; -var ReactElement; var ReactTestUtils; describe('ReactElement', function() { @@ -26,10 +28,9 @@ describe('ReactElement', function() { mocks = require('mocks'); React = require('React'); - ReactElement = require('ReactElement'); ReactTestUtils = require('ReactTestUtils'); ComponentClass = React.createClass({ - render: function() { return
; } + render: function() { return React.createElement('div'); } }); }); @@ -102,7 +103,9 @@ describe('ReactElement', function() { } }); - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument( + React.createElement(Wrapper) + ); expect(element._context).toEqual({ foo: 'bar' }); }); @@ -118,7 +121,9 @@ describe('ReactElement', function() { } }); - var instance = ReactTestUtils.renderIntoDocument(); + var instance = ReactTestUtils.renderIntoDocument( + React.createElement(Wrapper) + ); expect(element._owner.getPublicInstance()).toBe(instance); }); @@ -159,157 +164,6 @@ describe('ReactElement', function() { expect(console.warn.argsForCall.length).toBe(0); }); - it('warns for keys for arrays of elements in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - Component(null, [ Component(), Component() ]); - - expect(console.warn.argsForCall.length).toBe(1); - expect(console.warn.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' - ); - }); - - it('warns for keys for arrays of elements in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - var InnerClass = React.createClass({ - displayName: 'InnerClass', - render: function() { - return Component(null, this.props.childSet); - } - }); - - var InnerComponent = React.createFactory(InnerClass); - - var ComponentWrapper = React.createClass({ - displayName: 'ComponentWrapper', - render: function() { - return InnerComponent({ childSet: [ Component(), Component() ] }); - } - }); - - ReactTestUtils.renderIntoDocument(); - - expect(console.warn.argsForCall.length).toBe(1); - expect(console.warn.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop. ' + - 'Check the render method of InnerClass. ' + - 'It was passed a child from ComponentWrapper. ' - ); - }); - - it('warns for keys for iterables of elements in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - var iterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - var done = ++i > 2; - return { value: done ? undefined : Component(), done: done }; - } - }; - } - }; - - Component(null, iterable); - - expect(console.warn.argsForCall.length).toBe(1); - expect(console.warn.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' - ); - }); - - it('does not warns for arrays of elements with keys', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - Component(null, [ Component({key: '#1'}), Component({key: '#2'}) ]); - - expect(console.warn.argsForCall.length).toBe(0); - }); - - it('does not warns for iterable elements with keys', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - var iterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - var done = ++i > 2; - return { - value: done ? undefined : Component({key: '#' + i}), - done: done - }; - } - }; - } - }; - - Component(null, iterable); - - expect(console.warn.argsForCall.length).toBe(0); - }); - - it('warns for numeric keys on objects in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - Component(null, { 1: Component(), 2: Component() }); - - expect(console.warn.argsForCall.length).toBe(1); - expect(console.warn.argsForCall[0][0]).toContain( - 'Child objects should have non-numeric keys so ordering is preserved.' - ); - }); - - it('does not warn for numeric keys in entry iterables in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - var iterable = { - '@@iterator': function() { - var i = 0; - return { - next: function() { - var done = ++i > 2; - return { value: done ? undefined : [i, Component()], done: done }; - } - }; - } - }; - iterable.entries = iterable['@@iterator']; - - Component(null, iterable); - - expect(console.warn.argsForCall.length).toBe(0); - }); - - it('does not warn when the element is directly in rest args', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - Component(null, Component(), Component()); - - expect(console.warn.argsForCall.length).toBe(0); - }); - - it('does not warn when the array contains a non-element', function() { - spyOn(console, 'warn'); - var Component = React.createFactory(ComponentClass); - - Component(null, [ {}, {} ]); - - expect(console.warn.argsForCall.length).toBe(0); - }); - it('allows static methods to be called using the type property', function() { spyOn(console, 'warn'); @@ -323,11 +177,11 @@ describe('ReactElement', function() { return {valueToReturn: 'hi'}; }, render: function() { - return
; + return React.createElement('div'); } }); - var element = ; + var element = React.createElement(ComponentClass); expect(element.type.someStaticMethod()).toBe('someReturnValue'); expect(console.warn.argsForCall.length).toBe(0); }); @@ -335,22 +189,27 @@ describe('ReactElement', function() { it('identifies valid elements', function() { var Component = React.createClass({ render: function() { - return
; + return React.createElement('div'); } }); - expect(ReactElement.isValidElement(
)).toEqual(true); - expect(ReactElement.isValidElement()).toEqual(true); + expect(React.isValidElement(React.createElement('div'))) + .toEqual(true); + expect(React.isValidElement(React.createElement(Component))) + .toEqual(true); - expect(ReactElement.isValidElement(null)).toEqual(false); - expect(ReactElement.isValidElement(true)).toEqual(false); - expect(ReactElement.isValidElement({})).toEqual(false); - expect(ReactElement.isValidElement("string")).toEqual(false); - expect(ReactElement.isValidElement(React.DOM.div)).toEqual(false); - expect(ReactElement.isValidElement(Component)).toEqual(false); + expect(React.isValidElement(null)).toEqual(false); + expect(React.isValidElement(true)).toEqual(false); + expect(React.isValidElement({})).toEqual(false); + expect(React.isValidElement("string")).toEqual(false); + expect(React.isValidElement(React.DOM.div)).toEqual(false); + expect(React.isValidElement(Component)).toEqual(false); }); it('allows the use of PropTypes validators in statics', function() { + // TODO: This test was added to cover a special case where we proxied + // methods. However, we don't do that any more so this test can probably + // be removed. Leaving it in classic as a safety precausion. var Component = React.createClass({ render: () => null, statics: { @@ -362,12 +221,6 @@ describe('ReactElement', function() { expect(typeof Component.specialType.isRequired).toBe("function"); }); - it('allows a DOM element to be used with a string', function() { - var element = React.createElement('div', { className: 'foo' }); - var instance = ReactTestUtils.renderIntoDocument(element); - expect(instance.getDOMNode().tagName).toBe('DIV'); - }); - it('is indistinguishable from a plain object', function() { var element = React.createElement('div', { className: 'foo' }); var object = {}; @@ -380,50 +233,40 @@ describe('ReactElement', function() { return {fruit: 'persimmon'}; }, render: function() { - return ; + return React.createElement('span'); } }); var container = document.createElement('div'); var instance = React.render( - , + React.createElement(Component, { fruit: 'mango' }), container ); expect(instance.props.fruit).toBe('mango'); - React.render(, container); + React.render(React.createElement(Component), container); expect(instance.props.fruit).toBe('persimmon'); }); it('should normalize props with default values', function() { - var warn = console.warn; - console.warn = mocks.getMockFunction(); - var Component = React.createClass({ - propTypes: {prop: React.PropTypes.string.isRequired}, getDefaultProps: function() { return {prop: 'testKey'}; }, - getInitialState: function() { - return {prop: this.props.prop + 'State'}; - }, render: function() { - return {this.props.prop}; + return React.createElement('span', null, this.props.prop); } }); - var instance = ReactTestUtils.renderIntoDocument(); - expect(instance.props.prop).toBe('testKey'); - expect(instance.state.prop).toBe('testKeyState'); - - ReactTestUtils.renderIntoDocument(); - - expect(console.warn.mock.calls.length).toBe(1); - expect(console.warn.mock.calls[0][0]).toBe( - 'Warning: Required prop `prop` was not specified in `Component`.' + var instance = ReactTestUtils.renderIntoDocument( + React.createElement(Component) ); + expect(instance.props.prop).toBe('testKey'); - console.warn = warn; + var inst2 = ReactTestUtils.renderIntoDocument( + React.createElement(Component, { prop: null }) + ); + expect(inst2.props.prop).toBe(null); }); }); diff --git a/src/classic/element/__tests__/ReactElementValidator-test.js b/src/classic/element/__tests__/ReactElementValidator-test.js index 67198ac605..bccfa09c7c 100644 --- a/src/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/classic/element/__tests__/ReactElementValidator-test.js @@ -11,6 +11,9 @@ "use strict"; +// NOTE: We're explicitly not using JSX in this file. This is intended to test +// classic JS without JSX. + var React; var ReactTestUtils; @@ -23,10 +26,163 @@ describe('ReactElementValidator', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); ComponentClass = React.createClass({ - render: function() { return
; } + render: function() { return React.createElement('div'); } }); }); + it('warns for keys for arrays of elements in rest args', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + Component(null, [ Component(), Component() ]); + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.' + ); + }); + + it('warns for keys for arrays of elements with owner info', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + var InnerClass = React.createClass({ + displayName: 'InnerClass', + render: function() { + return Component(null, this.props.childSet); + } + }); + + var InnerComponent = React.createFactory(InnerClass); + + var ComponentWrapper = React.createClass({ + displayName: 'ComponentWrapper', + render: function() { + return InnerComponent({ childSet: [ Component(), Component() ] }); + } + }); + + ReactTestUtils.renderIntoDocument( + React.createElement(ComponentWrapper) + ); + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop. ' + + 'Check the render method of InnerClass. ' + + 'It was passed a child from ComponentWrapper. ' + ); + }); + + it('warns for keys for iterables of elements in rest args', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { value: done ? undefined : Component(), done: done }; + } + }; + } + }; + + Component(null, iterable); + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.' + ); + }); + + it('does not warns for arrays of elements with keys', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + Component(null, [ Component({key: '#1'}), Component({key: '#2'}) ]); + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warns for iterable elements with keys', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { + value: done ? undefined : Component({key: '#' + i}), + done: done + }; + } + }; + } + }; + + Component(null, iterable); + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('warns for numeric keys on objects in rest args', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + Component(null, { 1: Component(), 2: Component() }); + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Child objects should have non-numeric keys so ordering is preserved.' + ); + }); + + it('does not warn for numeric keys in entry iterables in rest args', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { value: done ? undefined : [i, Component()], done: done }; + } + }; + } + }; + iterable.entries = iterable['@@iterator']; + + Component(null, iterable); + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warn when the element is directly in rest args', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + Component(null, Component(), Component()); + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warn when the array contains a non-element', function() { + spyOn(console, 'warn'); + var Component = React.createFactory(ComponentClass); + + Component(null, [ {}, {} ]); + + expect(console.warn.argsForCall.length).toBe(0); + }); + // TODO: These warnings currently come from the composite component, but // they should be moved into the ReactElementValidator. @@ -40,15 +196,15 @@ describe('ReactElementValidator', function() { color: React.PropTypes.string }, render: function() { - return
My color is {this.color}
; + return React.createElement('div', null, 'My color is ' + this.color); } }); var ParentComp = React.createClass({ render: function() { - return ; + return React.createElement(MyComp, { color: 123 }); } }); - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)); expect(console.warn.calls[0].args[0]).toBe( 'Warning: Invalid prop `color` of type `number` supplied to `MyComp`, ' + 'expected `string`. Check the render method of `ParentComp`.' @@ -72,11 +228,8 @@ describe('ReactElementValidator', function() { ); React.createElement('div'); expect(console.warn.calls.length).toBe(2); - - expect(() => React.createElement(undefined)).not.toThrow() }); - it('should check default prop values', function() { spyOn(console, 'warn'); @@ -86,11 +239,34 @@ describe('ReactElementValidator', function() { return {prop: null}; }, render: function() { - return {this.props.prop}; + return React.createElement('span', null, this.props.prop); } }); - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument(React.createElement(Component)); + + expect(console.warn.calls.length).toBe(1); + expect(console.warn.calls[0].args[0]).toBe( + 'Warning: Required prop `prop` was not specified in `Component`.' + ); + }); + + it('should not check the default for explicit null', function() { + spyOn(console, 'warn'); + + var Component = React.createClass({ + propTypes: {prop: React.PropTypes.string.isRequired}, + getDefaultProps: function() { + return {prop: 'text'}; + }, + render: function() { + return React.createElement('span', null, this.props.prop); + } + }); + + ReactTestUtils.renderIntoDocument( + React.createElement(Component, {prop:null}) + ); expect(console.warn.calls.length).toBe(1); expect(console.warn.calls[0].args[0]).toBe( @@ -106,12 +282,16 @@ describe('ReactElementValidator', function() { prop: React.PropTypes.string.isRequired }, render: function() { - return {this.props.prop}; + return React.createElement('span', null, this.props.prop); } }); - ReactTestUtils.renderIntoDocument(); - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument( + React.createElement(Component) + ); + ReactTestUtils.renderIntoDocument( + React.createElement(Component, {prop: 42}) + ); expect(console.warn.calls.length).toBe(2); expect(console.warn.calls[0].args[0]).toBe( @@ -123,7 +303,9 @@ describe('ReactElementValidator', function() { '`Component`, expected `string`.' ); - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument( + React.createElement(Component, {prop: 'string'}) + ); // Should not error for strings expect(console.warn.calls.length).toBe(2); diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index f71de95c92..7875c3bdbe 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -32,8 +32,9 @@ function getDeclarationErrorAddendum(component) { var owner = component._currentElement._owner || null; if (owner) { var constructor = owner._instance.constructor; - if (constructor && constructor.displayName) { - return ' Check the render method of `' + constructor.displayName + '`.'; + var name = constructor && (constructor.displayName || constructor.name); + if (name) { + return ' Check the render method of `' + name + '`.'; } } return ''; @@ -510,7 +511,8 @@ var ReactCompositeComponentMixin = assign({}, _checkPropTypes: function(propTypes, props, location) { // TODO: Stop validating prop types here and only use the element // validation. - var componentName = this._instance.constructor.displayName; + var componentName = this._instance.constructor.displayName || + this._instance.constructor.name; for (var propName in propTypes) { if (propTypes.hasOwnProperty(propName)) { var error = diff --git a/src/modern/element/__tests__/ReactJSXElement-test.js b/src/modern/element/__tests__/ReactJSXElement-test.js new file mode 100644 index 0000000000..8044a7be69 --- /dev/null +++ b/src/modern/element/__tests__/ReactJSXElement-test.js @@ -0,0 +1,192 @@ +/** + * Copyright 2013-2014, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +"use strict"; + +var mocks; + +var React; +var ReactTestUtils; + +describe('ReactJSXElement', function() { + var Component; + + beforeEach(function() { + require('mock-modules').dumpCache(); + + mocks = require('mocks'); + + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + Component = class { + render() { return
; } + }; + }); + + it('returns a complete element according to spec', function() { + var element = ; + expect(element.type).toBe(Component); + expect(element.key).toBe(null); + expect(element.ref).toBe(null); + expect(element.props).toEqual({}); + }); + + it('allows a lower-case to be passed as the string type', function() { + var element =
; + expect(element.type).toBe('div'); + expect(element.key).toBe(null); + expect(element.ref).toBe(null); + expect(element.props).toEqual({}); + }); + + it('allows a string to be passed as the type', function() { + var TagName = 'div'; + var element = ; + expect(element.type).toBe('div'); + expect(element.key).toBe(null); + expect(element.ref).toBe(null); + expect(element.props).toEqual({}); + }); + + it('returns an immutable element', function() { + var element = ; + expect(() => element.type = 'div').toThrow(); + }); + + it('does not reuse the object that is spread into props', function() { + var config = { foo: 1 }; + var element = ; + expect(element.props.foo).toBe(1); + config.foo = 2; + expect(element.props.foo).toBe(1); + }); + + it('extracts key and ref from the rest of the props', function() { + var element = ; + expect(element.type).toBe(Component); + expect(element.key).toBe('12'); + expect(element.ref).toBe('34'); + expect(element.props).toEqual({foo:'56'}); + }); + + it('coerces the key to a string', function() { + var element = ; + expect(element.type).toBe(Component); + expect(element.key).toBe('12'); + expect(element.ref).toBe(null); + expect(element.props).toEqual({foo:'56'}); + }); + + it('merges JSX children onto the children prop', function() { + spyOn(console, 'warn'); + var a = 1; + var element = {a}; + expect(element.props.children).toBe(a); + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not override children if no JSX children are provided', function() { + spyOn(console, 'warn'); + var element = ; + expect(element.props.children).toBe('text'); + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('overrides children if null is provided as a JSX child', function() { + spyOn(console, 'warn'); + var element = {null}; + expect(element.props.children).toBe(null); + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('merges JSX children onto the children prop in an array', function() { + spyOn(console, 'warn'); + var a = 1, b = 2, c = 3; + var element = {a}{b}{c}; + expect(element.props.children).toEqual([1, 2, 3]); + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('allows static methods to be called using the type property', function() { + spyOn(console, 'warn'); + + class Component { + static someStaticMethod() { + return 'someReturnValue'; + } + render() { + return
; + } + } + + var element = ; + expect(element.type.someStaticMethod()).toBe('someReturnValue'); + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('identifies valid elements', function() { + class Component { + render() { + return
; + } + } + + expect(React.isValidElement(
)).toEqual(true); + expect(React.isValidElement()).toEqual(true); + + expect(React.isValidElement(null)).toEqual(false); + expect(React.isValidElement(true)).toEqual(false); + expect(React.isValidElement({})).toEqual(false); + expect(React.isValidElement("string")).toEqual(false); + expect(React.isValidElement(Component)).toEqual(false); + }); + + it('is indistinguishable from a plain object', function() { + var element =
; + var object = {}; + expect(element.constructor).toBe(object.constructor); + }); + + it('should use default prop value when removing a prop', function() { + class Component { + render() { + return ; + } + } + Component.defaultProps = { fruit: 'persimmon' }; + + var container = document.createElement('div'); + var instance = React.render( + , + container + ); + expect(instance.props.fruit).toBe('mango'); + + React.render(, container); + expect(instance.props.fruit).toBe('persimmon'); + }); + + it('should normalize props with default values', function() { + class Component { + render() { + return {this.props.prop}; + } + } + Component.defaultProps = { prop: 'testKey' }; + + var instance = ReactTestUtils.renderIntoDocument(); + expect(instance.props.prop).toBe('testKey'); + + var inst2 = ReactTestUtils.renderIntoDocument(); + expect(inst2.props.prop).toBe(null); + }); + +}); diff --git a/src/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/modern/element/__tests__/ReactJSXElementValidator-test.js new file mode 100644 index 0000000000..779c72fbfc --- /dev/null +++ b/src/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -0,0 +1,293 @@ +/** + * Copyright 2013-2014, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +"use strict"; + +// TODO: All these warnings should become static errors using Flow instead +// of dynamic errors when using JSX with Flow. + +var React; +var ReactTestUtils; + +describe('ReactJSXElementValidator', function() { + var Component; + + beforeEach(function() { + require('mock-modules').dumpCache(); + + React = require('React'); + ReactTestUtils = require('ReactTestUtils'); + + Component = class { + render() { return
; } + }; + }); + + it('warns for keys for arrays of elements in children position', function() { + spyOn(console, 'warn'); + + {[, ]}; + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.' + ); + }); + + it('warns for keys for arrays of elements with owner info', function() { + spyOn(console, 'warn'); + + class InnerComponent { + render() { + return {this.props.childSet}; + } + }; + + class ComponentWrapper { + render() { + return ( + , ]} + /> + ); + } + }; + + ReactTestUtils.renderIntoDocument(); + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop. ' + + 'Check the render method of InnerComponent. ' + + 'It was passed a child from ComponentWrapper. ' + ); + }); + + it('warns for keys for iterables of elements in rest args', function() { + spyOn(console, 'warn'); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { value: done ? undefined : , done: done }; + } + }; + } + }; + + {iterable}; + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.' + ); + }); + + it('does not warns for arrays of elements with keys', function() { + spyOn(console, 'warn'); + + {[ , ]}; + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warns for iterable elements with keys', function() { + spyOn(console, 'warn'); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { + value: done ? undefined : , + done: done + }; + } + }; + } + }; + + {iterable} + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('warns for numeric keys on objects as children', function() { + spyOn(console, 'warn'); + + {{ 1: , 2: }}; + + expect(console.warn.argsForCall.length).toBe(1); + expect(console.warn.argsForCall[0][0]).toContain( + 'Child objects should have non-numeric keys so ordering is preserved.' + ); + }); + + it('does not warn for numeric keys in entry iterable as a child', function() { + spyOn(console, 'warn'); + + var iterable = { + '@@iterator': function() { + var i = 0; + return { + next: function() { + var done = ++i > 2; + return { value: done ? undefined : [i, ], done: done }; + } + }; + } + }; + iterable.entries = iterable['@@iterator']; + + {iterable}; + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warn when the element is directly as children', function() { + spyOn(console, 'warn'); + + ; + + expect(console.warn.argsForCall.length).toBe(0); + }); + + it('does not warn when the child array contains non-elements', function() { + spyOn(console, 'warn'); + + {[ {}, {} ]}; + + expect(console.warn.argsForCall.length).toBe(0); + }); + + // TODO: These warnings currently come from the composite component, but + // they should be moved into the ReactElementValidator. + + it('should give context for PropType errors in nested components.', () => { + // In this test, we're making sure that if a proptype error is found in a + // component, we give a small hint as to which parent instantiated that + // component as per warnings about key usage in ReactElementValidator. + spyOn(console, 'warn'); + class MyComp { + render() { + return
My color is {this.color}
; + } + } + MyComp.propTypes = { + color: React.PropTypes.string + }; + class ParentComp { + render() { + return ; + } + } + ReactTestUtils.renderIntoDocument(); + expect(console.warn.calls[0].args[0]).toBe( + 'Warning: Invalid prop `color` of type `number` supplied to `MyComp`, ' + + 'expected `string`. Check the render method of `ParentComp`.' + ); + }); + + it('gives a helpful error when passing null or undefined', function() { + var Undefined = undefined; + var Null = null; + var Div = 'div'; + spyOn(console, 'warn'); + ; + ; + expect(console.warn.calls.length).toBe(2); + expect(console.warn.calls[0].args[0]).toContain( + 'type should not be null or undefined. It should be a string (for ' + + 'DOM elements) or a ReactClass (for composite components).' + ); + expect(console.warn.calls[1].args[0]).toContain( + 'type should not be null or undefined. It should be a string (for ' + + 'DOM elements) or a ReactClass (for composite components).' + ); +
; + expect(console.warn.calls.length).toBe(2); + }); + + it('should check default prop values', function() { + spyOn(console, 'warn'); + + class Component { + render() { + return {this.props.prop}; + } + } + Component.defaultProps = { prop: null }; + Component.propTypes = { prop: React.PropTypes.string.isRequired }; + + ReactTestUtils.renderIntoDocument(); + + expect(console.warn.calls.length).toBe(1); + expect(console.warn.calls[0].args[0]).toBe( + 'Warning: Required prop `prop` was not specified in `Component`.' + ); + }); + + it('should not check the default for explicit null', function() { + spyOn(console, 'warn'); + + class Component { + render() { + return {this.props.prop}; + } + } + Component.defaultProps = { prop: 'text' }; + Component.propTypes = { prop: React.PropTypes.string.isRequired }; + + ReactTestUtils.renderIntoDocument(); + + expect(console.warn.calls.length).toBe(1); + expect(console.warn.calls[0].args[0]).toBe( + 'Warning: Required prop `prop` was not specified in `Component`.' + ); + }); + + it('should check declared prop types', function() { + spyOn(console, 'warn'); + + class Component { + render() { + return {this.props.prop}; + } + } + Component.propTypes = { + prop: React.PropTypes.string.isRequired + }; + + ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument(); + + expect(console.warn.calls.length).toBe(2); + expect(console.warn.calls[0].args[0]).toBe( + 'Warning: Required prop `prop` was not specified in `Component`.' + ); + + expect(console.warn.calls[1].args[0]).toBe( + 'Warning: Invalid prop `prop` of type `number` supplied to ' + + '`Component`, expected `string`.' + ); + + ReactTestUtils.renderIntoDocument(); + + // Should not error for strings + expect(console.warn.calls.length).toBe(2); + }); + +});