diff --git a/src/ReactVersion.js b/src/ReactVersion.js index 1ecc489a8..cf91b4c58 100644 --- a/src/ReactVersion.js +++ b/src/ReactVersion.js @@ -13,2 +13,2 @@ -module.exports = '15.0.0'; +module.exports = '15.6.1'; diff --git a/src/addons/ReactAddonsDOMDependencies.js b/src/addons/ReactAddonsDOMDependencies.js new file mode 100644 index 000000000..70856ec6c --- /dev/null +++ b/src/addons/ReactAddonsDOMDependencies.js @@ -0,0 +1,37 @@ +/** + * Copyright 2013-present, 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. + * + * @providesModule ReactAddonsDOMDependencies + */ + +'use strict'; + +var ReactDOM = require('ReactDOM'); + +exports.getReactDOM = function() { + return ReactDOM; +}; + +if (__DEV__) { + var ReactPerf; + var ReactTestUtils; + + exports.getReactPerf = function() { + if (!ReactPerf) { + ReactPerf = require('ReactPerf'); + } + return ReactPerf; + }; + + exports.getReactTestUtils = function() { + if (!ReactTestUtils) { + ReactTestUtils = require('ReactTestUtils'); + } + return ReactTestUtils; + }; +} diff --git a/src/addons/ReactComponentWithPureRenderMixin.js b/src/addons/ReactComponentWithPureRenderMixin.js index afafbdaa7..cf00d7cb3 100644 --- a/src/addons/ReactComponentWithPureRenderMixin.js +++ b/src/addons/ReactComponentWithPureRenderMixin.js @@ -38,2 +38,4 @@ var shallowCompare = require('shallowCompare'); * use `forceUpdate()` when you know deep data structures have changed. + * + * See https://facebook.github.io/react/docs/pure-render-mixin.html */ diff --git a/src/addons/ReactDOMFactories.js b/src/addons/ReactDOMFactories.js new file mode 100644 index 000000000..f1f82c3a0 --- /dev/null +++ b/src/addons/ReactDOMFactories.js @@ -0,0 +1,169 @@ +/** + * Copyright 2013-present, 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. + * + * @providesModule ReactDOMFactories + */ + +'use strict'; + +var ReactElement = require('ReactElement'); + +/** + * Create a factory that creates HTML tag elements. + * + * @private + */ +var createDOMFactory = ReactElement.createFactory; +if (__DEV__) { + var ReactElementValidator = require('ReactElementValidator'); + createDOMFactory = ReactElementValidator.createFactory; +} + +/** + * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes. + * + * @public + */ +var ReactDOMFactories = { + a: createDOMFactory('a'), + abbr: createDOMFactory('abbr'), + address: createDOMFactory('address'), + area: createDOMFactory('area'), + article: createDOMFactory('article'), + aside: createDOMFactory('aside'), + audio: createDOMFactory('audio'), + b: createDOMFactory('b'), + base: createDOMFactory('base'), + bdi: createDOMFactory('bdi'), + bdo: createDOMFactory('bdo'), + big: createDOMFactory('big'), + blockquote: createDOMFactory('blockquote'), + body: createDOMFactory('body'), + br: createDOMFactory('br'), + button: createDOMFactory('button'), + canvas: createDOMFactory('canvas'), + caption: createDOMFactory('caption'), + cite: createDOMFactory('cite'), + code: createDOMFactory('code'), + col: createDOMFactory('col'), + colgroup: createDOMFactory('colgroup'), + data: createDOMFactory('data'), + datalist: createDOMFactory('datalist'), + dd: createDOMFactory('dd'), + del: createDOMFactory('del'), + details: createDOMFactory('details'), + dfn: createDOMFactory('dfn'), + dialog: createDOMFactory('dialog'), + div: createDOMFactory('div'), + dl: createDOMFactory('dl'), + dt: createDOMFactory('dt'), + em: createDOMFactory('em'), + embed: createDOMFactory('embed'), + fieldset: createDOMFactory('fieldset'), + figcaption: createDOMFactory('figcaption'), + figure: createDOMFactory('figure'), + footer: createDOMFactory('footer'), + form: createDOMFactory('form'), + h1: createDOMFactory('h1'), + h2: createDOMFactory('h2'), + h3: createDOMFactory('h3'), + h4: createDOMFactory('h4'), + h5: createDOMFactory('h5'), + h6: createDOMFactory('h6'), + head: createDOMFactory('head'), + header: createDOMFactory('header'), + hgroup: createDOMFactory('hgroup'), + hr: createDOMFactory('hr'), + html: createDOMFactory('html'), + i: createDOMFactory('i'), + iframe: createDOMFactory('iframe'), + img: createDOMFactory('img'), + input: createDOMFactory('input'), + ins: createDOMFactory('ins'), + kbd: createDOMFactory('kbd'), + keygen: createDOMFactory('keygen'), + label: createDOMFactory('label'), + legend: createDOMFactory('legend'), + li: createDOMFactory('li'), + link: createDOMFactory('link'), + main: createDOMFactory('main'), + map: createDOMFactory('map'), + mark: createDOMFactory('mark'), + menu: createDOMFactory('menu'), + menuitem: createDOMFactory('menuitem'), + meta: createDOMFactory('meta'), + meter: createDOMFactory('meter'), + nav: createDOMFactory('nav'), + noscript: createDOMFactory('noscript'), + object: createDOMFactory('object'), + ol: createDOMFactory('ol'), + optgroup: createDOMFactory('optgroup'), + option: createDOMFactory('option'), + output: createDOMFactory('output'), + p: createDOMFactory('p'), + param: createDOMFactory('param'), + picture: createDOMFactory('picture'), + pre: createDOMFactory('pre'), + progress: createDOMFactory('progress'), + q: createDOMFactory('q'), + rp: createDOMFactory('rp'), + rt: createDOMFactory('rt'), + ruby: createDOMFactory('ruby'), + s: createDOMFactory('s'), + samp: createDOMFactory('samp'), + script: createDOMFactory('script'), + section: createDOMFactory('section'), + select: createDOMFactory('select'), + small: createDOMFactory('small'), + source: createDOMFactory('source'), + span: createDOMFactory('span'), + strong: createDOMFactory('strong'), + style: createDOMFactory('style'), + sub: createDOMFactory('sub'), + summary: createDOMFactory('summary'), + sup: createDOMFactory('sup'), + table: createDOMFactory('table'), + tbody: createDOMFactory('tbody'), + td: createDOMFactory('td'), + textarea: createDOMFactory('textarea'), + tfoot: createDOMFactory('tfoot'), + th: createDOMFactory('th'), + thead: createDOMFactory('thead'), + time: createDOMFactory('time'), + title: createDOMFactory('title'), + tr: createDOMFactory('tr'), + track: createDOMFactory('track'), + u: createDOMFactory('u'), + ul: createDOMFactory('ul'), + var: createDOMFactory('var'), + video: createDOMFactory('video'), + wbr: createDOMFactory('wbr'), + + // SVG + circle: createDOMFactory('circle'), + clipPath: createDOMFactory('clipPath'), + defs: createDOMFactory('defs'), + ellipse: createDOMFactory('ellipse'), + g: createDOMFactory('g'), + image: createDOMFactory('image'), + line: createDOMFactory('line'), + linearGradient: createDOMFactory('linearGradient'), + mask: createDOMFactory('mask'), + path: createDOMFactory('path'), + pattern: createDOMFactory('pattern'), + polygon: createDOMFactory('polygon'), + polyline: createDOMFactory('polyline'), + radialGradient: createDOMFactory('radialGradient'), + rect: createDOMFactory('rect'), + stop: createDOMFactory('stop'), + svg: createDOMFactory('svg'), + text: createDOMFactory('text'), + tspan: createDOMFactory('tspan'), +}; + +module.exports = ReactDOMFactories; diff --git a/src/addons/ReactFragment.js b/src/addons/ReactFragment.js index f16ccf8bc..0ed35d4ae 100644 --- a/src/addons/ReactFragment.js +++ b/src/addons/ReactFragment.js @@ -33,4 +33,7 @@ var warnedAboutNumeric = false; var ReactFragment = { - // Wrap a keyed object in an opaque proxy that warns you if you access any - // of its properties. + /** + * Wrap a keyed object in an opaque proxy that warns you if you access any + * of its properties. + * See https://facebook.github.io/react/docs/create-fragment.html + */ create: function(object) { @@ -40,3 +43,3 @@ var ReactFragment = { 'React.addons.createFragment only accepts a single object. Got: %s', - object + object, ); @@ -48,3 +51,3 @@ var ReactFragment = { 'React.addons.createFragment does not accept a ReactElement ' + - 'without a wrapper object.' + 'without a wrapper object.', ); @@ -56,3 +59,3 @@ var ReactFragment = { 'React.addons.createFragment(...): Encountered an invalid child; DOM ' + - 'elements are not valid children of React components.' + 'elements are not valid children of React components.', ); @@ -67,3 +70,3 @@ var ReactFragment = { 'React.addons.createFragment(...): Child objects should have ' + - 'non-numeric keys so ordering is preserved.' + 'non-numeric keys so ordering is preserved.', ); @@ -76,3 +79,3 @@ var ReactFragment = { key, - emptyFunction.thatReturnsArgument + emptyFunction.thatReturnsArgument, ); diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js index bd13ea495..8cf081c98 100644 --- a/src/addons/ReactWithAddons.js +++ b/src/addons/ReactWithAddons.js @@ -15,4 +15,4 @@ var LinkedStateMixin = require('LinkedStateMixin'); var React = require('React'); -var ReactComponentWithPureRenderMixin = - require('ReactComponentWithPureRenderMixin'); +var ReactAddonsDOMDependencies = require('ReactAddonsDOMDependencies'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup'); @@ -36,4 +36,16 @@ React.addons = { if (__DEV__) { - React.addons.Perf = require('ReactDefaultPerf'); - React.addons.TestUtils = require('ReactTestUtils'); + // For the UMD build we get these lazily from the global since they're tied + // to the DOM renderer and it hasn't loaded yet. + Object.defineProperty(React.addons, 'Perf', { + enumerable: true, + get: function() { + return ReactAddonsDOMDependencies.getReactPerf(); + }, + }); + Object.defineProperty(React.addons, 'TestUtils', { + enumerable: true, + get: function() { + return ReactAddonsDOMDependencies.getReactTestUtils(); + }, + }); } diff --git a/src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js b/src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js deleted file mode 100644 index 2c9581994..000000000 --- a/src/addons/__tests__/ReactComponentWithPureRenderMixin-test.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright 2015-present, 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 React; -var ReactComponentWithPureRenderMixin; -var ReactTestUtils; - -describe('ReactComponentWithPureRenderMixin', function() { - - beforeEach(function() { - React = require('React'); - ReactComponentWithPureRenderMixin = - require('ReactComponentWithPureRenderMixin'); - ReactTestUtils = require('ReactTestUtils'); - }); - - it('provides a default shouldComponentUpdate implementation', function() { - var renderCalls = 0; - class PlasticWrap extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - color: 'green', - }; - } - - render() { - return ( - - ); - } - } - - var Apple = React.createClass({ - mixins: [ReactComponentWithPureRenderMixin], - - getInitialState: function() { - return { - cut: false, - slices: 1, - }; - }, - - cut: function() { - this.setState({ - cut: true, - slices: 10, - }); - }, - - eatSlice: function() { - this.setState({ - slices: this.state.slices - 1, - }); - }, - - render: function() { - renderCalls++; - return
; - }, - }); - - var instance = ReactTestUtils.renderIntoDocument(); - expect(renderCalls).toBe(1); - - // Do not re-render based on props - instance.setState({color: 'green'}); - expect(renderCalls).toBe(1); - - // Re-render based on props - instance.setState({color: 'red'}); - expect(renderCalls).toBe(2); - - // Re-render base on state - instance.refs.apple.cut(); - expect(renderCalls).toBe(3); - - // No re-render based on state - instance.refs.apple.cut(); - expect(renderCalls).toBe(3); - - // Re-render based on state again - instance.refs.apple.eatSlice(); - expect(renderCalls).toBe(4); - }); - - it('does not do a deep comparison', function() { - function getInitialState() { - return { - foo: [1, 2, 3], - bar: {a: 4, b: 5, c: 6}, - }; - } - - var renderCalls = 0; - var initialSettings = getInitialState(); - - var Component = React.createClass({ - mixins: [ReactComponentWithPureRenderMixin], - - getInitialState: function() { - return initialSettings; - }, - - render: function() { - renderCalls++; - return
; - }, - }); - - var instance = ReactTestUtils.renderIntoDocument(); - expect(renderCalls).toBe(1); - - // Do not re-render if state is equal - var settings = { - foo: initialSettings.foo, - bar: initialSettings.bar, - }; - instance.setState(settings); - expect(renderCalls).toBe(1); - - // Re-render because one field changed - initialSettings.foo = [1, 2, 3]; - instance.setState(initialSettings); - expect(renderCalls).toBe(2); - - // Re-render because the object changed - instance.setState(getInitialState()); - expect(renderCalls).toBe(3); - }); - -}); diff --git a/src/addons/__tests__/ReactDOMFactories-test.js b/src/addons/__tests__/ReactDOMFactories-test.js new file mode 100644 index 000000000..641faa3f0 --- /dev/null +++ b/src/addons/__tests__/ReactDOMFactories-test.js @@ -0,0 +1,41 @@ +/** + * Copyright 2013-present, 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 React = require('React'); +var {div} = require('ReactDOMFactories'); + +describe('ReactDOMFactories', () => { + it('allow factories to be called without warnings', () => { + spyOn(console, 'error'); + spyOn(console, 'warn'); + var element = div(); + expect(element.type).toBe('div'); + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + }); + + it('warns once when accessing React.DOM methods', () => { + spyOn(console, 'warn'); + + var a = React.DOM.a(); + var p = React.DOM.p(); + + expect(a.type).toBe('a'); + expect(p.type).toBe('p'); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.calls.first().args[0]).toContain( + 'Warning: Accessing factories like React.DOM.a has been deprecated', + ); + }); +}); diff --git a/src/addons/__tests__/ReactFragment-test.js b/src/addons/__tests__/ReactFragment-test.js index 4a1b9775d..1fc6f03d2 100644 --- a/src/addons/__tests__/ReactFragment-test.js +++ b/src/addons/__tests__/ReactFragment-test.js @@ -17,5 +17,4 @@ var ReactFragment; -describe('ReactFragment', function() { - - beforeEach(function() { +describe('ReactFragment', () => { + beforeEach(() => { React = require('React'); @@ -25,3 +24,3 @@ describe('ReactFragment', function() { - it('should throw if a plain object is used as a child', function() { + it('should throw if a plain object is used as a child', () => { var children = { @@ -33,7 +32,7 @@ describe('ReactFragment', function() { var container = document.createElement('div'); - expect(() => ReactDOM.render(element, container)).toThrow( + expect(() => ReactDOM.render(element, container)).toThrowError( 'Objects are not valid as a React child (found: object with keys ' + - '{x, y, z}). If you meant to render a collection of children, use an ' + - 'array instead or wrap the object using createFragment(object) from ' + - 'the React add-ons.' + '{x, y, z}). If you meant to render a collection of children, use an ' + + 'array instead or wrap the object using createFragment(object) from ' + + 'the React add-ons.', ); @@ -41,3 +40,3 @@ describe('ReactFragment', function() { - it('should throw if a plain object even if it is in an owner', function() { + it('should throw if a plain object even if it is in an owner', () => { class Foo extends React.Component { @@ -53,7 +52,7 @@ describe('ReactFragment', function() { var container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toThrow( + expect(() => ReactDOM.render(, container)).toThrowError( 'Objects are not valid as a React child (found: object with keys ' + - '{a, b, c}). If you meant to render a collection of children, use an ' + - 'array instead or wrap the object using createFragment(object) from ' + - 'the React add-ons. Check the render method of `Foo`.' + '{a, b, c}). If you meant to render a collection of children, use an ' + + 'array instead or wrap the object using createFragment(object) from ' + + 'the React add-ons. Check the render method of `Foo`.', ); @@ -61,10 +60,10 @@ describe('ReactFragment', function() { - it('should throw if a plain object looks like an old element', function() { + it('should throw if a plain object looks like an old element', () => { var oldEl = {_isReactElement: true, type: 'span', props: {}}; var container = document.createElement('div'); - expect(() => ReactDOM.render(
{oldEl}
, container)).toThrow( + expect(() => ReactDOM.render(
{oldEl}
, container)).toThrowError( 'Objects are not valid as a React child (found: object with keys ' + - '{_isReactElement, type, props}). It looks like you\'re using an ' + - 'element created by a different version of React. Make sure to use ' + - 'only one copy of React.' + "{_isReactElement, type, props}). It looks like you're using an " + + 'element created by a different version of React. Make sure to use ' + + 'only one copy of React.', ); @@ -72,3 +71,3 @@ describe('ReactFragment', function() { - it('warns for numeric keys on objects as children', function() { + it('warns for numeric keys on objects as children', () => { spyOn(console, 'error'); @@ -77,5 +76,5 @@ describe('ReactFragment', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Child objects should have non-numeric keys so ordering is preserved.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Child objects should have non-numeric keys so ordering is preserved.', ); @@ -83,8 +82,8 @@ describe('ReactFragment', function() { - it('should warn if passing null to createFragment', function() { + it('should warn if passing null to createFragment', () => { spyOn(console, 'error'); ReactFragment.create(null); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'React.addons.createFragment only accepts a single object.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'React.addons.createFragment only accepts a single object.', ); @@ -92,8 +91,8 @@ describe('ReactFragment', function() { - it('should warn if passing an array to createFragment', function() { + it('should warn if passing an array to createFragment', () => { spyOn(console, 'error'); ReactFragment.create([]); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'React.addons.createFragment only accepts a single object.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'React.addons.createFragment only accepts a single object.', ); @@ -101,12 +100,11 @@ describe('ReactFragment', function() { - it('should warn if passing a ReactElement to createFragment', function() { + it('should warn if passing a ReactElement to createFragment', () => { spyOn(console, 'error'); ReactFragment.create(
); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'React.addons.createFragment does not accept a ReactElement without a ' + - 'wrapper object.' + 'wrapper object.', ); }); - }); diff --git a/src/addons/__tests__/renderSubtreeIntoContainer-test.js b/src/addons/__tests__/renderSubtreeIntoContainer-test.js index de67b7ee4..0af5d7962 100644 --- a/src/addons/__tests__/renderSubtreeIntoContainer-test.js +++ b/src/addons/__tests__/renderSubtreeIntoContainer-test.js @@ -13,3 +13,5 @@ +var PropTypes = require('prop-types'); var React = require('React'); +var ReactDOM = require('ReactDOM'); var ReactTestUtils = require('ReactTestUtils'); @@ -17,24 +19,22 @@ var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer'); -describe('renderSubtreeIntoContainer', function() { - - it('should pass context when rendering subtree elsewhere', function() { - +describe('renderSubtreeIntoContainer', () => { + it('should pass context when rendering subtree elsewhere', () => { var portal = document.createElement('div'); - var Component = React.createClass({ - contextTypes: { - foo: React.PropTypes.string.isRequired, - }, + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; - render: function() { + render() { return
{this.context.foo}
; - }, - }); + } + } - var Parent = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string.isRequired, - }, + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; - getChildContext: function() { + getChildContext() { return { @@ -42,14 +42,16 @@ describe('renderSubtreeIntoContainer', function() { }; - }, + } - render: function() { + render() { return null; - }, + } - componentDidMount: function() { - expect(function() { - renderSubtreeIntoContainer(this, , portal); - }.bind(this)).not.toThrow(); - }, - }); + componentDidMount() { + expect( + function() { + renderSubtreeIntoContainer(this, , portal); + }.bind(this), + ).not.toThrow(); + } + } @@ -59,21 +61,24 @@ describe('renderSubtreeIntoContainer', function() { - it('should throw if parentComponent is invalid', function() { + it('should throw if parentComponent is invalid', () => { var portal = document.createElement('div'); - var Component = React.createClass({ - contextTypes: { - foo: React.PropTypes.string.isRequired, - }, + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; - render: function() { + render() { return
{this.context.foo}
; - }, - }); - - var Parent = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string.isRequired, - }, - - getChildContext: function() { + } + } + + // ESLint is confused here and thinks Parent is unused, presumably because + // it is only used inside of the class body? + // eslint-disable-next-line no-unused-vars + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; + + getChildContext() { return { @@ -81,14 +86,114 @@ describe('renderSubtreeIntoContainer', function() { }; - }, + } - render: function() { + render() { return null; - }, + } - componentDidMount: function() { + componentDidMount() { expect(function() { renderSubtreeIntoContainer(, , portal); - }).toThrow('parentComponentmust be a valid React Component'); - }, - }); + }).toThrowError('parentComponentmust be a valid React Component'); + } + } + }); + + it('should update context if it changes due to setState', () => { + var container = document.createElement('div'); + document.body.appendChild(container); + var portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + state = { + bar: 'initial', + }; + + getChildContext() { + return { + foo: this.state.bar, + getFoo: () => this.state.bar, + }; + } + + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } + + componentDidUpdate() { + renderSubtreeIntoContainer(this, , portal); + } + } + + var instance = ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('initial-initial'); + instance.setState({bar: 'changed'}); + expect(portal.firstChild.innerHTML).toBe('changed-changed'); + }); + + it('should update context if it changes due to re-render', () => { + var container = document.createElement('div'); + document.body.appendChild(container); + var portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + getChildContext() { + return { + foo: this.props.bar, + getFoo: () => this.props.bar, + }; + } + + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } + + componentDidUpdate() { + renderSubtreeIntoContainer(this, , portal); + } + } + + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('initial-initial'); + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('changed-changed'); }); diff --git a/src/addons/__tests__/update-test.js b/src/addons/__tests__/update-test.js index 05a6578ee..15cf7ff75 100644 --- a/src/addons/__tests__/update-test.js +++ b/src/addons/__tests__/update-test.js @@ -15,9 +15,8 @@ var update = require('update'); -describe('update', function() { - - describe('$push', function() { - it('pushes', function() { +describe('update', () => { + describe('$push', () => { + it('pushes', () => { expect(update([1], {$push: [7]})).toEqual([1, 7]); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = [1]; @@ -26,11 +25,11 @@ describe('update', function() { }); - it('only pushes an array', function() { - expect(update.bind(null, [], {$push: 7})).toThrow( + it('only pushes an array', () => { + expect(update.bind(null, [], {$push: 7})).toThrowError( 'update(): expected spec of $push to be an array; got 7. Did you ' + - 'forget to wrap your parameter in an array?' + 'forget to wrap your parameter in an array?', ); }); - it('only pushes unto an array', function() { - expect(update.bind(null, 1, {$push: 7})).toThrow( - 'update(): expected target of $push to be an array; got 1.' + it('only pushes unto an array', () => { + expect(update.bind(null, 1, {$push: 7})).toThrowError( + 'update(): expected target of $push to be an array; got 1.', ); @@ -39,7 +38,7 @@ describe('update', function() { - describe('$unshift', function() { - it('unshifts', function() { + describe('$unshift', () => { + it('unshifts', () => { expect(update([1], {$unshift: [7]})).toEqual([7, 1]); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = [1]; @@ -48,11 +47,11 @@ describe('update', function() { }); - it('only unshifts an array', function() { - expect(update.bind(null, [], {$unshift: 7})).toThrow( + it('only unshifts an array', () => { + expect(update.bind(null, [], {$unshift: 7})).toThrowError( 'update(): expected spec of $unshift to be an array; got 7. Did you ' + - 'forget to wrap your parameter in an array?' + 'forget to wrap your parameter in an array?', ); }); - it('only unshifts unto an array', function() { - expect(update.bind(null, 1, {$unshift: 7})).toThrow( - 'update(): expected target of $unshift to be an array; got 1.' + it('only unshifts unto an array', () => { + expect(update.bind(null, 1, {$unshift: 7})).toThrowError( + 'update(): expected target of $unshift to be an array; got 1.', ); @@ -61,7 +60,7 @@ describe('update', function() { - describe('$splice', function() { - it('splices', function() { + describe('$splice', () => { + it('splices', () => { expect(update([1, 4, 3], {$splice: [[1, 1, 2]]})).toEqual([1, 2, 3]); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = [1, 4, 3]; @@ -70,15 +69,15 @@ describe('update', function() { }); - it('only splices an array of arrays', function() { - expect(update.bind(null, [], {$splice: 1})).toThrow( + it('only splices an array of arrays', () => { + expect(update.bind(null, [], {$splice: 1})).toThrowError( 'update(): expected spec of $splice to be an array of arrays; got 1. ' + - 'Did you forget to wrap your parameters in an array?' + 'Did you forget to wrap your parameters in an array?', ); - expect(update.bind(null, [], {$splice: [1]})).toThrow( + expect(update.bind(null, [], {$splice: [1]})).toThrowError( 'update(): expected spec of $splice to be an array of arrays; got 1. ' + - 'Did you forget to wrap your parameters in an array?' + 'Did you forget to wrap your parameters in an array?', ); }); - it('only splices unto an array', function() { - expect(update.bind(null, 1, {$splice: 7})).toThrow( - 'Expected $splice target to be an array; got 1' + it('only splices unto an array', () => { + expect(update.bind(null, 1, {$splice: 7})).toThrowError( + 'Expected $splice target to be an array; got 1', ); @@ -87,7 +86,7 @@ describe('update', function() { - describe('$merge', function() { - it('merges', function() { + describe('$merge', () => { + it('merges', () => { expect(update({a: 'b'}, {$merge: {c: 'd'}})).toEqual({a: 'b', c: 'd'}); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = {a: 'b'}; @@ -96,10 +95,10 @@ describe('update', function() { }); - it('only merges with an object', function() { - expect(update.bind(null, {}, {$merge: 7})).toThrow( - 'update(): $merge expects a spec of type \'object\'; got 7' + it('only merges with an object', () => { + expect(update.bind(null, {}, {$merge: 7})).toThrowError( + "update(): $merge expects a spec of type 'object'; got 7", ); }); - it('only merges with an object', function() { - expect(update.bind(null, 7, {$merge: {a: 'b'}})).toThrow( - 'update(): $merge expects a target of type \'object\'; got 7' + it('only merges with an object', () => { + expect(update.bind(null, 7, {$merge: {a: 'b'}})).toThrowError( + "update(): $merge expects a target of type 'object'; got 7", ); @@ -108,7 +107,7 @@ describe('update', function() { - describe('$set', function() { - it('sets', function() { + describe('$set', () => { + it('sets', () => { expect(update({a: 'b'}, {$set: {c: 'd'}})).toEqual({c: 'd'}); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = {a: 'b'}; @@ -119,3 +118,3 @@ describe('update', function() { - describe('$apply', function() { + describe('$apply', () => { var applier = function(node) { @@ -123,6 +122,6 @@ describe('update', function() { }; - it('applies', function() { + it('applies', () => { expect(update({v: 2}, {$apply: applier})).toEqual({v: 4}); }); - it('does not mutate the original object', function() { + it('does not mutate the original object', () => { var obj = {v: 2}; @@ -131,5 +130,5 @@ describe('update', function() { }); - it('only applies a function', function() { - expect(update.bind(null, 2, {$apply: 123})).toThrow( - 'update(): expected spec of $apply to be a function; got 123.' + it('only applies a function', () => { + expect(update.bind(null, 2, {$apply: 123})).toThrowError( + 'update(): expected spec of $apply to be a function; got 123.', ); @@ -138,23 +137,28 @@ describe('update', function() { - it('should support deep updates', function() { - expect(update({ - a: 'b', - c: { - d: 'e', - f: [1], - g: [2], - h: [3], - i: {j: 'k'}, - l: 4, - }, - }, { - c: { - d: {$set: 'm'}, - f: {$push: [5]}, - g: {$unshift: [6]}, - h: {$splice: [[0, 1, 7]]}, - i: {$merge: {n: 'o'}}, - l: {$apply: (x) => x * 2}, - }, - })).toEqual({ + it('should support deep updates', () => { + expect( + update( + { + a: 'b', + c: { + d: 'e', + f: [1], + g: [2], + h: [3], + i: {j: 'k'}, + l: 4, + }, + }, + { + c: { + d: {$set: 'm'}, + f: {$push: [5]}, + g: {$unshift: [6]}, + h: {$splice: [[0, 1, 7]]}, + i: {$merge: {n: 'o'}}, + l: {$apply: x => x * 2}, + }, + }, + ), + ).toEqual({ a: 'b', @@ -171,7 +175,7 @@ describe('update', function() { - it('should require a command', function() { - expect(update.bind(null, {a: 'b'}, {a: 'c'})).toThrow( + it('should require a command', () => { + expect(update.bind(null, {a: 'b'}, {a: 'c'})).toThrowError( 'update(): You provided a key path to update() that did not contain ' + - 'one of $push, $unshift, $splice, $set, $merge, $apply. Did you ' + - 'forget to include {$set: ...}?' + 'one of $push, $unshift, $splice, $set, $merge, $apply. Did you ' + + 'forget to include {$set: ...}?', ); @@ -179,5 +183,5 @@ describe('update', function() { - it('should perform safe hasOwnProperty check', function() { - expect(update({}, {'hasOwnProperty': {$set: 'a'}})).toEqual({ - 'hasOwnProperty': 'a', + it('should perform safe hasOwnProperty check', () => { + expect(update({}, {hasOwnProperty: {$set: 'a'}})).toEqual({ + hasOwnProperty: 'a', }); diff --git a/src/addons/link/LinkedStateMixin.js b/src/addons/link/LinkedStateMixin.js index c45a79faa..4a646095d 100644 --- a/src/addons/link/LinkedStateMixin.js +++ b/src/addons/link/LinkedStateMixin.js @@ -18,2 +18,3 @@ var ReactStateSetters = require('ReactStateSetters'); * A simple mixin around ReactLink.forState(). + * See https://facebook.github.io/react/docs/two-way-binding-helpers.html */ @@ -25,4 +26,3 @@ var LinkedStateMixin = { * - * @param {string} key state key to update. Note: you may want to use keyOf() - * if you're using Google Closure Compiler advanced mode. + * @param {string} key state key to update. * @return {ReactLink} ReactLink instance linking to the state. @@ -32,3 +32,3 @@ var LinkedStateMixin = { this.state[key], - ReactStateSetters.createStateKeySetter(this, key) + ReactStateSetters.createStateKeySetter(this, key), ); diff --git a/src/addons/link/ReactLink.js b/src/addons/link/ReactLink.js index ea6435c92..d91345e69 100644 --- a/src/addons/link/ReactLink.js +++ b/src/addons/link/ReactLink.js @@ -36,5 +36,6 @@ -var React = require('React'); - /** + * Deprecated: An an easy way to express two-way binding with React. + * See https://facebook.github.io/react/docs/two-way-binding-helpers.html + * * @param {*} value current value of the link @@ -47,24 +48,2 @@ function ReactLink(value, requestChange) { -/** - * Creates a PropType that enforces the ReactLink API and optionally checks the - * type of the value being passed inside the link. Example: - * - * MyComponent.propTypes = { - * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number) - * } - */ -function createLinkTypeChecker(linkType) { - var shapes = { - value: linkType === undefined ? - React.PropTypes.any.isRequired : - linkType.isRequired, - requestChange: React.PropTypes.func.isRequired, - }; - return React.PropTypes.shape(shapes); -} - -ReactLink.PropTypes = { - link: createLinkTypeChecker, -}; - module.exports = ReactLink; diff --git a/src/renderers/shared/reconciler/ReactStateSetters.js b/src/addons/link/ReactStateSetters.js similarity index 100% rename from src/renderers/shared/reconciler/ReactStateSetters.js rename to src/addons/link/ReactStateSetters.js diff --git a/src/addons/link/__tests__/LinkedStateMixin-test.js b/src/addons/link/__tests__/LinkedStateMixin-test.js deleted file mode 100644 index 01b82351c..000000000 --- a/src/addons/link/__tests__/LinkedStateMixin-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright 2013-present, 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'; - - -describe('LinkedStateMixin', function() { - var LinkedStateMixin; - var React; - var ReactTestUtils; - - beforeEach(function() { - LinkedStateMixin = require('LinkedStateMixin'); - React = require('React'); - ReactTestUtils = require('ReactTestUtils'); - }); - - it('should create a ReactLink for state', function() { - var Component = React.createClass({ - mixins: [LinkedStateMixin], - - getInitialState: function() { - return {value: 'initial value'}; - }, - - render: function() { - return value is {this.state.value}; - }, - }); - var component = ReactTestUtils.renderIntoDocument(); - var link = component.linkState('value'); - expect(component.state.value).toBe('initial value'); - expect(link.value).toBe('initial value'); - link.requestChange('new value'); - expect(component.state.value).toBe('new value'); - expect(component.linkState('value').value).toBe('new value'); - }); -}); diff --git a/src/addons/link/__tests__/ReactLinkPropTypes-test.js b/src/addons/link/__tests__/ReactLinkPropTypes-test.js deleted file mode 100644 index 6ade13777..000000000 --- a/src/addons/link/__tests__/ReactLinkPropTypes-test.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright 2013-present, 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 emptyFunction = require('emptyFunction'); -var LinkPropTypes = require('ReactLink').PropTypes; -var React = require('React'); -var ReactPropTypeLocations = require('ReactPropTypeLocations'); - -var invalidMessage = 'Invalid prop `testProp` supplied to `testComponent`.'; -var requiredMessage = - 'Required prop `testProp` was not specified in `testComponent`.'; - -function typeCheckFail(declaration, value, message) { - var props = {testProp: value}; - var error = declaration( - props, - 'testProp', - 'testComponent', - ReactPropTypeLocations.prop - ); - expect(error instanceof Error).toBe(true); - expect(error.message).toBe(message); -} - -function typeCheckPass(declaration, value) { - var props = {testProp: value}; - var error = declaration( - props, - 'testProp', - 'testComponent', - ReactPropTypeLocations.prop - ); - expect(error).toBe(null); -} - -describe('ReactLink', function() { - it('should fail if the argument does not implement the Link API', function() { - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {}, - 'Required prop `testProp.value` was not specified in `testComponent`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {value: 123}, - 'Required prop `testProp.requestChange` was not specified in `testComponent`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {requestChange: emptyFunction}, - 'Required prop `testProp.value` was not specified in `testComponent`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {value: null, requestChange: null}, - 'Required prop `testProp.value` was not specified in `testComponent`.' - ); - }); - - it('should allow valid links even if no type was specified', function() { - typeCheckPass( - LinkPropTypes.link(), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(), - {value: {}, requestChange: emptyFunction, - }); - }); - - it('should allow no link to be passed at all', function() { - typeCheckPass( - LinkPropTypes.link(React.PropTypes.string), - undefined - ); - }); - - it('should allow valid links with correct value format', function() { - typeCheckPass( - LinkPropTypes.link(React.PropTypes.any), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.number), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.node), - {value: 42, requestChange: emptyFunction} - ); - }); - - it('should fail if the link`s value type does not match', function() { - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string), - {value: 123, requestChange: emptyFunction}, - 'Invalid prop `testProp.value` of type `number` supplied to `testComponent`,' + - ' expected `string`.' - ); - }); - - it('should be implicitly optional and not warn without values', function() { - typeCheckPass(LinkPropTypes.link(), null); - typeCheckPass(LinkPropTypes.link(), undefined); - typeCheckPass(LinkPropTypes.link(React.PropTypes.string), null); - typeCheckPass(LinkPropTypes.link(React.PropTypes.string), undefined); - }); - - it('should warn for missing required values', function() { - typeCheckFail(LinkPropTypes.link().isRequired, null, requiredMessage); - typeCheckFail(LinkPropTypes.link().isRequired, undefined, requiredMessage); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string).isRequired, - null, - requiredMessage - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string).isRequired, - undefined, - requiredMessage - ); - }); - - it('should be compatible with React.PropTypes.oneOfType', function() { - typeCheckPass( - React.PropTypes.oneOfType([LinkPropTypes.link(React.PropTypes.number)]), - {value: 123, requestChange: emptyFunction} - ); - typeCheckFail( - React.PropTypes.oneOfType([LinkPropTypes.link(React.PropTypes.number)]), - 123, - invalidMessage - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.oneOfType([React.PropTypes.number])), - {value: 123, requestChange: emptyFunction} - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.oneOfType([React.PropTypes.number])), - {value: 'imastring', requestChange: emptyFunction}, - 'Invalid prop `testProp.value` supplied to `testComponent`.' - ); - }); -}); diff --git a/src/addons/shallowCompare.js b/src/addons/shallowCompare.js index 33e4591ba..334f7a2db 100644 --- a/src/addons/shallowCompare.js +++ b/src/addons/shallowCompare.js @@ -8,4 +8,4 @@ * -* @providesModule shallowCompare -*/ + * @providesModule shallowCompare + */ @@ -18,2 +18,3 @@ var shallowEqual = require('shallowEqual'); * See ReactComponentWithPureRenderMixin + * See also https://facebook.github.io/react/docs/shallow-compare.html */ diff --git a/src/addons/transitions/ReactCSSTransitionGroup.js b/src/addons/transitions/ReactCSSTransitionGroup.js index 4177ec790..4c1fb3b6e 100644 --- a/src/addons/transitions/ReactCSSTransitionGroup.js +++ b/src/addons/transitions/ReactCSSTransitionGroup.js @@ -14,2 +14,4 @@ var React = require('React'); +var propTypesFactory = require('prop-types/factory'); +var PropTypes = propTypesFactory(React.isValidElement); @@ -28,12 +30,15 @@ function createTransitionTimeoutPropValidator(transitionType) { return new Error( - timeoutPropName + ' wasn\'t supplied to ReactCSSTransitionGroup: ' + - 'this can cause unreliable animations and won\'t be supported in ' + - 'a future version of React. See ' + - 'https://fb.me/react-animation-transition-group-timeout for more ' + - 'information.' + timeoutPropName + + " wasn't supplied to ReactCSSTransitionGroup: " + + "this can cause unreliable animations and won't be supported in " + + 'a future version of React. See ' + + 'https://fb.me/react-animation-transition-group-timeout for more ' + + 'information.', ); - // If the duration isn't a number + // If the duration isn't a number } else if (typeof props[timeoutPropName] !== 'number') { - return new Error(timeoutPropName + ' must be a number (in milliseconds)'); + return new Error( + timeoutPropName + ' must be a number (in milliseconds)', + ); } @@ -43,11 +48,16 @@ function createTransitionTimeoutPropValidator(transitionType) { -var ReactCSSTransitionGroup = React.createClass({ - displayName: 'ReactCSSTransitionGroup', +/** + * An easy way to perform CSS transitions and animations when a React component + * enters or leaves the DOM. + * See https://facebook.github.io/react/docs/animation.html#high-level-api-reactcsstransitiongroup + */ +class ReactCSSTransitionGroup extends React.Component { + static displayName = 'ReactCSSTransitionGroup'; - propTypes: { + static propTypes = { transitionName: ReactCSSTransitionGroupChild.propTypes.name, - transitionAppear: React.PropTypes.bool, - transitionEnter: React.PropTypes.bool, - transitionLeave: React.PropTypes.bool, + transitionAppear: PropTypes.bool, + transitionEnter: PropTypes.bool, + transitionLeave: PropTypes.bool, transitionAppearTimeout: createTransitionTimeoutPropValidator('Appear'), @@ -55,13 +65,11 @@ var ReactCSSTransitionGroup = React.createClass({ transitionLeaveTimeout: createTransitionTimeoutPropValidator('Leave'), - }, + }; - getDefaultProps: function() { - return { - transitionAppear: false, - transitionEnter: true, - transitionLeave: true, - }; - }, + static defaultProps = { + transitionAppear: false, + transitionEnter: true, + transitionLeave: true, + }; - _wrapChild: function(child) { + _wrapChild = child => { // We need to provide this childFactory so that @@ -80,13 +88,13 @@ var ReactCSSTransitionGroup = React.createClass({ }, - child + child, ); - }, + }; - render: function() { + render() { return React.createElement( ReactTransitionGroup, - Object.assign({}, this.props, {childFactory: this._wrapChild}) + Object.assign({}, this.props, {childFactory: this._wrapChild}), ); - }, -}); + } +} diff --git a/src/addons/transitions/ReactCSSTransitionGroupChild.js b/src/addons/transitions/ReactCSSTransitionGroupChild.js index 170cb9d87..94bf1fd6f 100644 --- a/src/addons/transitions/ReactCSSTransitionGroupChild.js +++ b/src/addons/transitions/ReactCSSTransitionGroupChild.js @@ -14,3 +14,6 @@ var React = require('React'); -var ReactDOM = require('ReactDOM'); +var ReactAddonsDOMDependencies = require('ReactAddonsDOMDependencies'); + +var propTypesFactory = require('prop-types/factory'); +var PropTypes = propTypesFactory(React.isValidElement); @@ -23,20 +26,18 @@ var TICK = 17; -var ReactCSSTransitionGroupChild = React.createClass({ - displayName: 'ReactCSSTransitionGroupChild', - - propTypes: { - name: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.shape({ - enter: React.PropTypes.string, - leave: React.PropTypes.string, - active: React.PropTypes.string, +class ReactCSSTransitionGroupChild extends React.Component { + static propTypes = { + name: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + enter: PropTypes.string, + leave: PropTypes.string, + active: PropTypes.string, }), - React.PropTypes.shape({ - enter: React.PropTypes.string, - enterActive: React.PropTypes.string, - leave: React.PropTypes.string, - leaveActive: React.PropTypes.string, - appear: React.PropTypes.string, - appearActive: React.PropTypes.string, + PropTypes.shape({ + enter: PropTypes.string, + enterActive: PropTypes.string, + leave: PropTypes.string, + leaveActive: PropTypes.string, + appear: PropTypes.string, + appearActive: PropTypes.string, }), @@ -47,12 +48,14 @@ var ReactCSSTransitionGroupChild = React.createClass({ // or a bool for the timeout flags (appearTimeout etc.) - appear: React.PropTypes.bool, - enter: React.PropTypes.bool, - leave: React.PropTypes.bool, - appearTimeout: React.PropTypes.number, - enterTimeout: React.PropTypes.number, - leaveTimeout: React.PropTypes.number, - }, + appear: PropTypes.bool, + enter: PropTypes.bool, + leave: PropTypes.bool, + appearTimeout: PropTypes.number, + enterTimeout: PropTypes.number, + leaveTimeout: PropTypes.number, + }; - transition: function(animationType, finishCallback, userSpecifiedDelay) { - var node = ReactDOM.findDOMNode(this); + _isMounted = false; + + transition = (animationType, finishCallback, userSpecifiedDelay) => { + var node = ReactAddonsDOMDependencies.getReactDOM().findDOMNode(this); @@ -65,4 +68,6 @@ var ReactCSSTransitionGroupChild = React.createClass({ - var className = this.props.name[animationType] || this.props.name + '-' + animationType; - var activeClassName = this.props.name[animationType + 'Active'] || className + '-active'; + var className = + this.props.name[animationType] || this.props.name + '-' + animationType; + var activeClassName = + this.props.name[animationType + 'Active'] || className + '-active'; var timeout = null; @@ -91,3 +96,3 @@ var ReactCSSTransitionGroupChild = React.createClass({ // Need to do this to actually trigger a transition. - this.queueClass(activeClassName); + this.queueClassAndNode(activeClassName, node); @@ -102,28 +107,37 @@ var ReactCSSTransitionGroupChild = React.createClass({ } - }, + }; - queueClass: function(className) { - this.classNameQueue.push(className); + queueClassAndNode = (className, node) => { + this.classNameAndNodeQueue.push({ + className: className, + node: node, + }); if (!this.timeout) { - this.timeout = setTimeout(this.flushClassNameQueue, TICK); + this.timeout = setTimeout(this.flushClassNameAndNodeQueue, TICK); } - }, + }; - flushClassNameQueue: function() { - if (this.isMounted()) { - this.classNameQueue.forEach( - CSSCore.addClass.bind(CSSCore, ReactDOM.findDOMNode(this)) - ); + flushClassNameAndNodeQueue = () => { + if (this._isMounted) { + this.classNameAndNodeQueue.forEach(function(obj) { + CSSCore.addClass(obj.node, obj.className); + }); } - this.classNameQueue.length = 0; + this.classNameAndNodeQueue.length = 0; this.timeout = null; - }, + }; - componentWillMount: function() { - this.classNameQueue = []; + componentWillMount() { + this.classNameAndNodeQueue = []; this.transitionTimeouts = []; - }, + } + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; - componentWillUnmount: function() { if (this.timeout) { @@ -134,5 +148,7 @@ var ReactCSSTransitionGroupChild = React.createClass({ }); - }, - componentWillAppear: function(done) { + this.classNameAndNodeQueue.length = 0; + } + + componentWillAppear = done => { if (this.props.appear) { @@ -142,5 +158,5 @@ var ReactCSSTransitionGroupChild = React.createClass({ } - }, + }; - componentWillEnter: function(done) { + componentWillEnter = done => { if (this.props.enter) { @@ -150,5 +166,5 @@ var ReactCSSTransitionGroupChild = React.createClass({ } - }, + }; - componentWillLeave: function(done) { + componentWillLeave = done => { if (this.props.leave) { @@ -158,8 +174,8 @@ var ReactCSSTransitionGroupChild = React.createClass({ } - }, + }; - render: function() { + render() { return onlyChild(this.props.children); - }, -}); + } +} diff --git a/src/addons/transitions/ReactTransitionChildMapping.js b/src/addons/transitions/ReactTransitionChildMapping.js index 285834324..4ba6795df 100644 --- a/src/addons/transitions/ReactTransitionChildMapping.js +++ b/src/addons/transitions/ReactTransitionChildMapping.js @@ -21,5 +21,6 @@ var ReactTransitionChildMapping = { * @param {*} children `this.props.children` + * @param {number=} selfDebugID Optional debugID of the current internal instance. * @return {object} Mapping of key to child */ - getChildMapping: function(children) { + getChildMapping: function(children, selfDebugID) { if (!children) { @@ -27,2 +28,7 @@ var ReactTransitionChildMapping = { } + + if (__DEV__) { + return flattenChildren(children, selfDebugID); + } + return flattenChildren(children); @@ -82,3 +88,3 @@ var ReactTransitionChildMapping = { childMapping[nextKeysPending[nextKey][i]] = getValueForKey( - pendingNextKey + pendingNextKey, ); diff --git a/src/addons/transitions/ReactTransitionGroup.js b/src/addons/transitions/ReactTransitionGroup.js index 6d6df1a80..cd576aa85 100644 --- a/src/addons/transitions/ReactTransitionGroup.js +++ b/src/addons/transitions/ReactTransitionGroup.js @@ -16,26 +16,31 @@ var ReactTransitionChildMapping = require('ReactTransitionChildMapping'); +var propTypesFactory = require('prop-types/factory'); +var PropTypes = propTypesFactory(React.isValidElement); + var emptyFunction = require('emptyFunction'); -var ReactTransitionGroup = React.createClass({ - displayName: 'ReactTransitionGroup', +/** + * A basis for animations. When children are declaratively added or removed, + * special lifecycle hooks are called. + * See https://facebook.github.io/react/docs/animation.html#low-level-api-reacttransitiongroup + */ +class ReactTransitionGroup extends React.Component { + static displayName = 'ReactTransitionGroup'; - propTypes: { - component: React.PropTypes.any, - childFactory: React.PropTypes.func, - }, + static propTypes = { + component: PropTypes.any, + childFactory: PropTypes.func, + }; - getDefaultProps: function() { - return { - component: 'span', - childFactory: emptyFunction.thatReturnsArgument, - }; - }, + static defaultProps = { + component: 'span', + childFactory: emptyFunction.thatReturnsArgument, + }; - getInitialState: function() { - return { - children: ReactTransitionChildMapping.getChildMapping(this.props.children), - }; - }, + state = { + // TODO: can we get useful debug information to show at this point? + children: ReactTransitionChildMapping.getChildMapping(this.props.children), + }; - componentWillMount: function() { + componentWillMount() { this.currentlyTransitioningKeys = {}; @@ -43,5 +48,5 @@ var ReactTransitionGroup = React.createClass({ this.keysToLeave = []; - }, + } - componentDidMount: function() { + componentDidMount() { var initialChildMapping = this.state.children; @@ -52,7 +57,7 @@ var ReactTransitionGroup = React.createClass({ } - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { var nextChildMapping = ReactTransitionChildMapping.getChildMapping( - nextProps.children + nextProps.children, ); @@ -63,3 +68,3 @@ var ReactTransitionGroup = React.createClass({ prevChildMapping, - nextChildMapping + nextChildMapping, ), @@ -71,4 +76,7 @@ var ReactTransitionGroup = React.createClass({ var hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key); - if (nextChildMapping[key] && !hasPrev && - !this.currentlyTransitioningKeys[key]) { + if ( + nextChildMapping[key] && + !hasPrev && + !this.currentlyTransitioningKeys[key] + ) { this.keysToEnter.push(key); @@ -79,4 +87,7 @@ var ReactTransitionGroup = React.createClass({ var hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key); - if (prevChildMapping[key] && !hasNext && - !this.currentlyTransitioningKeys[key]) { + if ( + prevChildMapping[key] && + !hasNext && + !this.currentlyTransitioningKeys[key] + ) { this.keysToLeave.push(key); @@ -86,5 +97,5 @@ var ReactTransitionGroup = React.createClass({ // If we want to someday check for reordering, we could do it here. - }, + } - componentDidUpdate: function() { + componentDidUpdate() { var keysToEnter = this.keysToEnter; @@ -96,5 +107,5 @@ var ReactTransitionGroup = React.createClass({ keysToLeave.forEach(this.performLeave); - }, + } - performAppear: function(key) { + performAppear = key => { this.currentlyTransitioningKeys[key] = true; @@ -104,5 +115,3 @@ var ReactTransitionGroup = React.createClass({ if (component.componentWillAppear) { - component.componentWillAppear( - this._handleDoneAppearing.bind(this, key) - ); + component.componentWillAppear(this._handleDoneAppearing.bind(this, key)); } else { @@ -110,5 +119,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - _handleDoneAppearing: function(key) { + _handleDoneAppearing = key => { var component = this.refs[key]; @@ -121,3 +130,3 @@ var ReactTransitionGroup = React.createClass({ var currentChildMapping = ReactTransitionChildMapping.getChildMapping( - this.props.children + this.props.children, ); @@ -128,5 +137,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - performEnter: function(key) { + performEnter = key => { this.currentlyTransitioningKeys[key] = true; @@ -136,5 +145,3 @@ var ReactTransitionGroup = React.createClass({ if (component.componentWillEnter) { - component.componentWillEnter( - this._handleDoneEntering.bind(this, key) - ); + component.componentWillEnter(this._handleDoneEntering.bind(this, key)); } else { @@ -142,5 +149,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - _handleDoneEntering: function(key) { + _handleDoneEntering = key => { var component = this.refs[key]; @@ -153,3 +160,3 @@ var ReactTransitionGroup = React.createClass({ var currentChildMapping = ReactTransitionChildMapping.getChildMapping( - this.props.children + this.props.children, ); @@ -160,5 +167,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - performLeave: function(key) { + performLeave = key => { this.currentlyTransitioningKeys[key] = true; @@ -174,5 +181,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - _handleDoneLeaving: function(key) { + _handleDoneLeaving = key => { var component = this.refs[key]; @@ -186,3 +193,3 @@ var ReactTransitionGroup = React.createClass({ var currentChildMapping = ReactTransitionChildMapping.getChildMapping( - this.props.children + this.props.children, ); @@ -199,5 +206,5 @@ var ReactTransitionGroup = React.createClass({ } - }, + }; - render: function() { + render() { // TODO: we could get rid of the need for the wrapper node @@ -213,15 +220,26 @@ var ReactTransitionGroup = React.createClass({ // leaving. - childrenToRender.push(React.cloneElement( - this.props.childFactory(child), - {ref: key, key: key} - )); + childrenToRender.push( + React.cloneElement(this.props.childFactory(child), { + ref: key, + key: key, + }), + ); } } - return React.createElement( - this.props.component, - this.props, - childrenToRender - ); - }, -}); + + // Do not forward ReactTransitionGroup props to primitive DOM nodes + var props = Object.assign({}, this.props); + delete props.transitionLeave; + delete props.transitionName; + delete props.transitionAppear; + delete props.transitionEnter; + delete props.childFactory; + delete props.transitionLeaveTimeout; + delete props.transitionEnterTimeout; + delete props.transitionAppearTimeout; + delete props.component; + + return React.createElement(this.props.component, props, childrenToRender); + } +} diff --git a/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js b/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js deleted file mode 100644 index 0bf8c6bb2..000000000 --- a/src/addons/transitions/__tests__/ReactCSSTransitionGroup-test.js +++ /dev/null @@ -1,294 +0,0 @@ -/** - * Copyright 2013-present, 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 CSSCore = require('CSSCore'); - -var React; -var ReactDOM; -var ReactCSSTransitionGroup; - -// Most of the real functionality is covered in other unit tests, this just -// makes sure we're wired up correctly. -describe('ReactCSSTransitionGroup', function() { - var container; - - beforeEach(function() { - jest.resetModuleRegistry(); - React = require('React'); - ReactDOM = require('ReactDOM'); - ReactCSSTransitionGroup = require('ReactCSSTransitionGroup'); - - container = document.createElement('div'); - spyOn(console, 'error'); - }); - - it('should warn if timeouts aren\'t specified', function() { - ReactDOM.render( - - - , - container - ); - - // Warning about the missing transitionLeaveTimeout prop - expect(console.error.argsForCall.length).toBe(1); - }); - - it('should not warn if timeouts is zero', function() { - ReactDOM.render( - - - , - container - ); - - expect(console.error.argsForCall.length).toBe(0); - }); - - it('should clean-up silently after the timeout elapses', function() { - var a = ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - - setTimeout.mock.calls.length = 0; - - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one'); - - // For some reason jst is adding extra setTimeout()s and grunt test isn't, - // so we need to do this disgusting hack. - for (var i = 0; i < setTimeout.mock.calls.length; i++) { - if (setTimeout.mock.calls[i][1] === 200) { - setTimeout.mock.calls[i][0](); - break; - } - } - - // No warnings - expect(console.error.argsForCall.length).toBe(0); - - // The leaving child has been removed - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); - }); - - it('should keep both sets of DOM nodes around', function() { - var a = ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one'); - }); - - it('should switch transitionLeave from false to true', function() { - var a = ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('three'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('two'); - }); - - it('should work with no children', function() { - ReactDOM.render( - , - container - ); - }); - - it('should work with a null child', function() { - ReactDOM.render( - - {[null]} - , - container - ); - }); - - it('should transition from one to null', function() { - var a = ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( - - {null} - , - container - ); - // (Here, we expect the original child to stick around but test that no - // exception is thrown) - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one'); - }); - - it('should transition from false to one', function() { - var a = ReactDOM.render( - - {false} - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(0); - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one'); - }); - - it('should use transition-type specific names when they\'re provided', function() { - var customTransitionNames = { - enter: 'custom-entering', - leave: 'custom-leaving', - }; - - var a = ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - - // Add an element - ReactDOM.render( - - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - - var enteringNode = ReactDOM.findDOMNode(a).childNodes[1]; - expect(CSSCore.hasClass(enteringNode, 'custom-entering')).toBe(true); - - // Remove an element - ReactDOM.render( - - - , - container - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - - var leavingNode = ReactDOM.findDOMNode(a).childNodes[0]; - expect(CSSCore.hasClass(leavingNode, 'custom-leaving')).toBe(true); - }); - - it('should clear transition timeouts when unmounted', function() { - var Component = React.createClass({ - render: function() { - return ( - - {this.props.children} - - ); - }, - }); - - ReactDOM.render(, container); - ReactDOM.render(, container); - - ReactDOM.unmountComponentAtNode(container); - - // Testing that no exception is thrown here, as the timeout has been cleared. - jest.runAllTimers(); - }); -}); diff --git a/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js b/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js index 7cc3c63a6..b6c705898 100644 --- a/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js +++ b/src/addons/transitions/__tests__/ReactTransitionChildMapping-test.js @@ -16,4 +16,4 @@ var ReactTransitionChildMapping; -describe('ReactTransitionChildMapping', function() { - beforeEach(function() { +describe('ReactTransitionChildMapping', () => { + beforeEach(() => { React = require('React'); @@ -22,3 +22,3 @@ describe('ReactTransitionChildMapping', function() { - it('should support getChildMapping', function() { + it('should support getChildMapping', () => { var oneone =
; @@ -29,3 +29,3 @@ describe('ReactTransitionChildMapping', function() { expect( - ReactTransitionChildMapping.getChildMapping(component.props.children) + ReactTransitionChildMapping.getChildMapping(component.props.children), ).toEqual({ @@ -36,3 +36,3 @@ describe('ReactTransitionChildMapping', function() { - it('should support mergeChildMappings for adding keys', function() { + it('should support mergeChildMappings for adding keys', () => { var prev = { @@ -53,3 +53,3 @@ describe('ReactTransitionChildMapping', function() { - it('should support mergeChildMappings for removing keys', function() { + it('should support mergeChildMappings for removing keys', () => { var prev = { @@ -70,3 +70,3 @@ describe('ReactTransitionChildMapping', function() { - it('should support mergeChildMappings for adding and removing', function() { + it('should support mergeChildMappings for adding and removing', () => { var prev = { @@ -89,3 +89,3 @@ describe('ReactTransitionChildMapping', function() { - it('should reconcile overlapping insertions and deletions', function() { + it('should reconcile overlapping insertions and deletions', () => { var prev = { @@ -111,3 +111,3 @@ describe('ReactTransitionChildMapping', function() { - it('should support mergeChildMappings with undefined input', function() { + it('should support mergeChildMappings with undefined input', () => { var prev = { diff --git a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js b/src/addons/transitions/__tests__/ReactTransitionGroup-test.js deleted file mode 100644 index f98302be1..000000000 --- a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js +++ /dev/null @@ -1,272 +0,0 @@ -/** - * Copyright 2013-present, 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 React; -var ReactDOM; -var ReactTransitionGroup; - -// Most of the real functionality is covered in other unit tests, this just -// makes sure we're wired up correctly. -describe('ReactTransitionGroup', function() { - var container; - - beforeEach(function() { - React = require('React'); - ReactDOM = require('ReactDOM'); - ReactTransitionGroup = require('ReactTransitionGroup'); - - container = document.createElement('div'); - }); - - - it('should handle willEnter correctly', function() { - var log = []; - - var Child = React.createClass({ - componentDidMount: function() { - log.push('didMount'); - }, - componentWillAppear: function(cb) { - log.push('willAppear'); - cb(); - }, - componentDidAppear: function() { - log.push('didAppear'); - }, - componentWillEnter: function(cb) { - log.push('willEnter'); - cb(); - }, - componentDidEnter: function() { - log.push('didEnter'); - }, - componentWillLeave: function(cb) { - log.push('willLeave'); - cb(); - }, - componentDidLeave: function() { - log.push('didLeave'); - }, - componentWillUnmount: function() { - log.push('willUnmount'); - }, - render: function() { - return ; - }, - }); - - var Component = React.createClass({ - getInitialState: function() { - return {count: 1}; - }, - render: function() { - var children = []; - for (var i = 0; i < this.state.count; i++) { - children.push(); - } - return {children}; - }, - }); - - var instance = ReactDOM.render(, container); - expect(log).toEqual(['didMount', 'willAppear', 'didAppear']); - - log = []; - instance.setState({count: 2}, function() { - expect(log).toEqual(['didMount', 'willEnter', 'didEnter']); - - log = []; - instance.setState({count: 1}, function() { - expect(log).toEqual(['willLeave', 'didLeave', 'willUnmount']); - }); - }); - }); - - it('should handle enter/leave/enter/leave correctly', function() { - var log = []; - var willEnterCb; - - var Child = React.createClass({ - componentDidMount: function() { - log.push('didMount'); - }, - componentWillEnter: function(cb) { - log.push('willEnter'); - willEnterCb = cb; - }, - componentDidEnter: function() { - log.push('didEnter'); - }, - componentWillLeave: function(cb) { - log.push('willLeave'); - cb(); - }, - componentDidLeave: function() { - log.push('didLeave'); - }, - componentWillUnmount: function() { - log.push('willUnmount'); - }, - render: function() { - return ; - }, - }); - - var Component = React.createClass({ - getInitialState: function() { - return {count: 1}; - }, - render: function() { - var children = []; - for (var i = 0; i < this.state.count; i++) { - children.push(); - } - return {children}; - }, - }); - - var instance = ReactDOM.render(, container); - expect(log).toEqual(['didMount']); - instance.setState({count: 2}); - expect(log).toEqual(['didMount', 'didMount', 'willEnter']); - for (var k = 0; k < 5; k++) { - instance.setState({count: 2}); - expect(log).toEqual(['didMount', 'didMount', 'willEnter']); - instance.setState({count: 1}); - } - // other animations are blocked until willEnterCb is called - willEnterCb(); - expect(log).toEqual([ - 'didMount', 'didMount', 'willEnter', - 'didEnter', 'willLeave', 'didLeave', 'willUnmount', - ]); - }); - - it('should handle enter/leave/enter correctly', function() { - var log = []; - var willEnterCb; - - var Child = React.createClass({ - componentDidMount: function() { - log.push('didMount'); - }, - componentWillEnter: function(cb) { - log.push('willEnter'); - willEnterCb = cb; - }, - componentDidEnter: function() { - log.push('didEnter'); - }, - componentWillLeave: function(cb) { - log.push('willLeave'); - cb(); - }, - componentDidLeave: function() { - log.push('didLeave'); - }, - componentWillUnmount: function() { - log.push('willUnmount'); - }, - render: function() { - return ; - }, - }); - - var Component = React.createClass({ - getInitialState: function() { - return {count: 1}; - }, - render: function() { - var children = []; - for (var i = 0; i < this.state.count; i++) { - children.push(); - } - return {children}; - }, - }); - - var instance = ReactDOM.render(, container); - expect(log).toEqual(['didMount']); - instance.setState({count: 2}); - expect(log).toEqual(['didMount', 'didMount', 'willEnter']); - for (var k = 0; k < 5; k++) { - instance.setState({count: 1}); - expect(log).toEqual(['didMount', 'didMount', 'willEnter']); - instance.setState({count: 2}); - } - willEnterCb(); - expect(log).toEqual([ - 'didMount', 'didMount', 'willEnter', 'didEnter', - ]); - }); - - it('should handle entering/leaving several elements at once', function() { - var log = []; - - var Child = React.createClass({ - componentDidMount: function() { - log.push('didMount' + this.props.id); - }, - componentWillEnter: function(cb) { - log.push('willEnter' + this.props.id); - cb(); - }, - componentDidEnter: function() { - log.push('didEnter' + this.props.id); - }, - componentWillLeave: function(cb) { - log.push('willLeave' + this.props.id); - cb(); - }, - componentDidLeave: function() { - log.push('didLeave' + this.props.id); - }, - componentWillUnmount: function() { - log.push('willUnmount' + this.props.id); - }, - render: function() { - return ; - }, - }); - - var Component = React.createClass({ - getInitialState: function() { - return {count: 1}; - }, - render: function() { - var children = []; - for (var i = 0; i < this.state.count; i++) { - children.push(); - } - return {children}; - }, - }); - - var instance = ReactDOM.render(, container); - expect(log).toEqual(['didMount0']); - log = []; - - instance.setState({count: 3}); - expect(log).toEqual([ - 'didMount1', 'didMount2', 'willEnter1', 'didEnter1', - 'willEnter2', 'didEnter2', - ]); - log = []; - - instance.setState({count: 0}); - expect(log).toEqual([ - 'willLeave0', 'didLeave0', 'willLeave1', 'didLeave1', - 'willLeave2', 'didLeave2', 'willUnmount0', 'willUnmount1', 'willUnmount2', - ]); - }); -}); diff --git a/src/addons/update.js b/src/addons/update.js index cc2434e2b..ce5fa018d 100644 --- a/src/addons/update.js +++ b/src/addons/update.js @@ -11,3 +11,3 @@ - /* global hasOwnProperty:true */ +/* global hasOwnProperty:true */ @@ -15,3 +15,2 @@ -var keyOf = require('keyOf'); var invariant = require('invariant'); @@ -29,8 +28,8 @@ function shallowCopy(x) { -var COMMAND_PUSH = keyOf({$push: null}); -var COMMAND_UNSHIFT = keyOf({$unshift: null}); -var COMMAND_SPLICE = keyOf({$splice: null}); -var COMMAND_SET = keyOf({$set: null}); -var COMMAND_MERGE = keyOf({$merge: null}); -var COMMAND_APPLY = keyOf({$apply: null}); +var COMMAND_PUSH = '$push'; +var COMMAND_UNSHIFT = '$unshift'; +var COMMAND_SPLICE = '$splice'; +var COMMAND_SET = '$set'; +var COMMAND_MERGE = '$merge'; +var COMMAND_APPLY = '$apply'; @@ -56,3 +55,3 @@ function invariantArrayCase(value, spec, command) { command, - value + value, ); @@ -62,5 +61,5 @@ function invariantArrayCase(value, spec, command) { 'update(): expected spec of %s to be an array; got %s. ' + - 'Did you forget to wrap your parameter in an array?', + 'Did you forget to wrap your parameter in an array?', command, - specValue + specValue, ); @@ -68,2 +67,6 @@ function invariantArrayCase(value, spec, command) { +/** + * Returns a updated shallow copy of an object without mutating the original. + * See https://facebook.github.io/react/docs/update.html for details. + */ function update(value, spec) { @@ -72,5 +75,5 @@ function update(value, spec) { 'update(): You provided a key path to update() that did not contain one ' + - 'of %s. Did you forget to include {%s: ...}?', + 'of %s. Did you forget to include {%s: ...}?', ALL_COMMANDS_LIST.join(', '), - COMMAND_SET + COMMAND_SET, ); @@ -81,3 +84,3 @@ function update(value, spec) { 'Cannot have more than one key in an object with %s', - COMMAND_SET + COMMAND_SET, ); @@ -93,5 +96,5 @@ function update(value, spec) { mergeObj && typeof mergeObj === 'object', - 'update(): %s expects a spec of type \'object\'; got %s', + "update(): %s expects a spec of type 'object'; got %s", COMMAND_MERGE, - mergeObj + mergeObj, ); @@ -99,5 +102,5 @@ function update(value, spec) { nextValue && typeof nextValue === 'object', - 'update(): %s expects a target of type \'object\'; got %s', + "update(): %s expects a target of type 'object'; got %s", COMMAND_MERGE, - nextValue + nextValue, ); @@ -125,3 +128,3 @@ function update(value, spec) { COMMAND_SPLICE, - value + value, ); @@ -130,5 +133,5 @@ function update(value, spec) { 'update(): expected spec of %s to be an array of arrays; got %s. ' + - 'Did you forget to wrap your parameters in an array?', + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, - spec[COMMAND_SPLICE] + spec[COMMAND_SPLICE], ); @@ -138,5 +141,5 @@ function update(value, spec) { 'update(): expected spec of %s to be an array of arrays; got %s. ' + - 'Did you forget to wrap your parameters in an array?', + 'Did you forget to wrap your parameters in an array?', COMMAND_SPLICE, - spec[COMMAND_SPLICE] + spec[COMMAND_SPLICE], ); @@ -151,3 +154,3 @@ function update(value, spec) { COMMAND_APPLY, - spec[COMMAND_APPLY] + spec[COMMAND_APPLY], ); diff --git a/src/isomorphic/React.js b/src/isomorphic/React.js index 551e0a608..29cfea6da 100644 --- a/src/isomorphic/React.js +++ b/src/isomorphic/React.js @@ -13,8 +13,6 @@ +var ReactBaseClasses = require('ReactBaseClasses'); var ReactChildren = require('ReactChildren'); -var ReactComponent = require('ReactComponent'); -var ReactClass = require('ReactClass'); var ReactDOMFactories = require('ReactDOMFactories'); var ReactElement = require('ReactElement'); -var ReactElementValidator = require('ReactElementValidator'); var ReactPropTypes = require('ReactPropTypes'); @@ -22,2 +20,3 @@ var ReactVersion = require('ReactVersion'); +var createReactClass = require('createClass'); var onlyChild = require('onlyChild'); @@ -29,2 +28,6 @@ var cloneElement = ReactElement.cloneElement; if (__DEV__) { + var lowPriorityWarning = require('lowPriorityWarning'); + var canDefineProperty = require('canDefineProperty'); + var ReactElementValidator = require('ReactElementValidator'); + var didWarnPropTypesDeprecated = false; createElement = ReactElementValidator.createElement; @@ -34,4 +37,36 @@ if (__DEV__) { -var React = { +var __spread = Object.assign; +var createMixin = function(mixin) { + return mixin; +}; + +if (__DEV__) { + var warnedForSpread = false; + var warnedForCreateMixin = false; + __spread = function() { + lowPriorityWarning( + warnedForSpread, + 'React.__spread is deprecated and should not be used. Use ' + + 'Object.assign directly or another helper function with similar ' + + 'semantics. You may be seeing this warning due to your compiler. ' + + 'See https://fb.me/react-spread-deprecation for more details.', + ); + warnedForSpread = true; + return Object.assign.apply(null, arguments); + }; + createMixin = function(mixin) { + lowPriorityWarning( + warnedForCreateMixin, + 'React.createMixin is deprecated and should not be used. ' + + 'In React v16.0, it will be removed. ' + + 'You can use this mixin directly instead. ' + + 'See https://fb.me/createmixin-was-never-implemented for more info.', + ); + warnedForCreateMixin = true; + return mixin; + }; +} + +var React = { // Modern @@ -46,3 +81,4 @@ var React = { - Component: ReactComponent, + Component: ReactBaseClasses.Component, + PureComponent: ReactBaseClasses.PureComponent, @@ -55,8 +91,5 @@ var React = { PropTypes: ReactPropTypes, - createClass: ReactClass.createClass, + createClass: createReactClass, createFactory: createFactory, - createMixin: function(mixin) { - // Currently a noop. Will be used to validate and trace mixins. - return mixin; - }, + createMixin: createMixin, @@ -67,4 +100,66 @@ var React = { version: ReactVersion, + + // Deprecated hook for JSX spread, don't use this for anything. + __spread: __spread, }; +if (__DEV__) { + let warnedForCreateClass = false; + if (canDefineProperty) { + Object.defineProperty(React, 'PropTypes', { + get() { + lowPriorityWarning( + didWarnPropTypesDeprecated, + 'Accessing PropTypes via the main React package is deprecated,' + + ' and will be removed in React v16.0.' + + ' Use the latest available v15.* prop-types package from npm instead.' + + ' For info on usage, compatibility, migration and more, see ' + + 'https://fb.me/prop-types-docs', + ); + didWarnPropTypesDeprecated = true; + return ReactPropTypes; + }, + }); + + Object.defineProperty(React, 'createClass', { + get: function() { + lowPriorityWarning( + warnedForCreateClass, + 'Accessing createClass via the main React package is deprecated,' + + ' and will be removed in React v16.0.' + + " Use a plain JavaScript class instead. If you're not yet " + + 'ready to migrate, create-react-class v15.* is available ' + + 'on npm as a temporary, drop-in replacement. ' + + 'For more info see https://fb.me/react-create-class', + ); + warnedForCreateClass = true; + return createReactClass; + }, + }); + } + + // React.DOM factories are deprecated. Wrap these methods so that + // invocations of the React.DOM namespace and alert users to switch + // to the `react-dom-factories` package. + React.DOM = {}; + var warnedForFactories = false; + Object.keys(ReactDOMFactories).forEach(function(factory) { + React.DOM[factory] = function(...args) { + if (!warnedForFactories) { + lowPriorityWarning( + false, + 'Accessing factories like React.DOM.%s has been deprecated ' + + 'and will be removed in v16.0+. Use the ' + + 'react-dom-factories package instead. ' + + ' Version 1.0 provides a drop-in replacement.' + + ' For more info, see https://fb.me/react-dom-factories', + factory, + ); + warnedForFactories = true; + } + return ReactDOMFactories[factory](...args); + }; + }); +} + module.exports = React; diff --git a/src/isomorphic/ReactDebugInstanceMap.js b/src/isomorphic/ReactDebugInstanceMap.js deleted file mode 100644 index 50dddf4ad..000000000 --- a/src/isomorphic/ReactDebugInstanceMap.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright 2016-present, 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. - * - * @providesModule ReactDebugInstanceMap - */ - -'use strict'; - -var warning = require('warning'); - -function checkValidInstance(internalInstance) { - if (!internalInstance) { - warning( - false, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received %s. ' + - 'Please report this as a bug in React.', - internalInstance - ); - return false; - } - var isValid = typeof internalInstance.mountComponent === 'function'; - warning( - isValid, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received an object with the following ' + - 'keys: %s. Please report this as a bug in React.', - Object.keys(internalInstance).join(', ') - ); - return isValid; -} - -var idCounter = 1; -var instancesByIDs = {}; -var instancesToIDs; - -function getIDForInstance(internalInstance) { - if (!instancesToIDs) { - instancesToIDs = new WeakMap(); - } - if (instancesToIDs.has(internalInstance)) { - return instancesToIDs.get(internalInstance); - } else { - var instanceID = (idCounter++).toString(); - instancesToIDs.set(internalInstance, instanceID); - return instanceID; - } -} - -function getInstanceByID(instanceID) { - return instancesByIDs[instanceID] || null; -} - -function isRegisteredInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - return instancesByIDs.hasOwnProperty(instanceID); - } else { - return false; - } -} - -function registerInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - instancesByIDs[instanceID] = internalInstance; - } -} - -function unregisterInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - delete instancesByIDs[instanceID]; - } -} - -var ReactDebugInstanceMap = { - getIDForInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return null; - } - return getIDForInstance(internalInstance); - }, - getInstanceByID(instanceID) { - return getInstanceByID(instanceID); - }, - isRegisteredInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return false; - } - return isRegisteredInstance(internalInstance); - }, - registerInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - !isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - registerInstance(internalInstance); - }, - unregisterInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - unregisterInstance(internalInstance); - }, -}; - -module.exports = ReactDebugInstanceMap; diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js deleted file mode 100644 index 4d2c2a353..000000000 --- a/src/isomorphic/ReactDebugTool.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright 2016-present, 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. - * - * @providesModule ReactDebugTool - */ - -'use strict'; - -var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool'); -var warning = require('warning'); - -var eventHandlers = []; -var handlerDoesThrowForEvent = {}; - -function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) { - if (__DEV__) { - eventHandlers.forEach(function(handler) { - try { - if (handler[handlerFunctionName]) { - handler[handlerFunctionName](arg1, arg2, arg3, arg4, arg5); - } - } catch (e) { - warning( - !handlerDoesThrowForEvent[handlerFunctionName], - 'exception thrown by devtool while handling %s: %s', - handlerFunctionName, - e.message - ); - handlerDoesThrowForEvent[handlerFunctionName] = true; - } - }); - } -} - -var ReactDebugTool = { - addDevtool(devtool) { - eventHandlers.push(devtool); - }, - removeDevtool(devtool) { - for (var i = 0; i < eventHandlers.length; i++) { - if (eventHandlers[i] === devtool) { - eventHandlers.splice(i, 1); - i--; - } - } - }, - onBeginProcessingChildContext() { - emitEvent('onBeginProcessingChildContext'); - }, - onEndProcessingChildContext() { - emitEvent('onEndProcessingChildContext'); - }, - onSetState() { - emitEvent('onSetState'); - }, - onMountRootComponent(internalInstance) { - emitEvent('onMountRootComponent', internalInstance); - }, - onMountComponent(internalInstance) { - emitEvent('onMountComponent', internalInstance); - }, - onUpdateComponent(internalInstance) { - emitEvent('onUpdateComponent', internalInstance); - }, - onUnmountComponent(internalInstance) { - emitEvent('onUnmountComponent', internalInstance); - }, -}; - -ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool); - -module.exports = ReactDebugTool; diff --git a/src/isomorphic/__tests__/React-test.js b/src/isomorphic/__tests__/React-test.js new file mode 100644 index 000000000..7a2d0c9ac --- /dev/null +++ b/src/isomorphic/__tests__/React-test.js @@ -0,0 +1,71 @@ +/** + * Copyright 2013-present, 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'; + +describe('React', () => { + var React; + + beforeEach(() => { + React = require('React'); + }); + + it('should log a deprecation warning once when using React.__spread', () => { + spyOn(console, 'warn'); + React.__spread({}); + React.__spread({}); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'React.__spread is deprecated and should not be used', + ); + }); + + it('should log a deprecation warning once when using React.createMixin', () => { + spyOn(console, 'warn'); + React.createMixin(); + React.createMixin(); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'React.createMixin is deprecated and should not be used', + ); + }); + + it('should warn once when attempting to access React.createClass', () => { + spyOn(console, 'warn'); + let createClass = React.createClass; + createClass = React.createClass; + expect(createClass).not.toBe(undefined); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'Warning: Accessing createClass via the main React package is ' + + 'deprecated, and will be removed in React v16.0. ' + + "Use a plain JavaScript class instead. If you're not yet ready " + + 'to migrate, create-react-class v15.* is available on npm as ' + + 'a temporary, drop-in replacement. ' + + 'For more info see https://fb.me/react-create-class', + ); + }); + + it('should warn once when attempting to access React.PropTypes', () => { + spyOn(console, 'warn'); + let PropTypes = React.PropTypes; + PropTypes = React.PropTypes; + expect(PropTypes).not.toBe(undefined); + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'Warning: Accessing PropTypes via the main React package is ' + + 'deprecated, and will be removed in React v16.0. ' + + 'Use the latest available v15.* prop-types package from ' + + 'npm instead. For info on usage, compatibility, migration ' + + 'and more, see https://fb.me/prop-types-docs', + ); + }); +}); diff --git a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js deleted file mode 100644 index d9a063e2c..000000000 --- a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright 2016-present, 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'; - -describe('ReactDebugInstanceMap', function() { - var React; - var ReactDebugInstanceMap; - var ReactDOM; - - beforeEach(function() { - jest.resetModuleRegistry(); - React = require('React'); - ReactDebugInstanceMap = require('ReactDebugInstanceMap'); - ReactDOM = require('ReactDOM'); - }); - - function createStubInstance() { - return { mountComponent: () => {} }; - } - - it('should register and unregister instances', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(true); - - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.unregisterInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - }); - - it('should assign stable IDs', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(typeof inst1ID).toBe('string'); - expect(typeof inst2ID).toBe('string'); - expect(inst1ID).not.toBe(inst2ID); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - }); - - it('should retrieve registered instance by its ID', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(inst1); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(inst2); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - }); - - it('should warn when registering an instance twice', function() { - spyOn(console, 'error'); - - var inst = createStubInstance(); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(0); - - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.unregisterInstance(inst); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - }); - - it('should warn when unregistering an instance twice', function() { - spyOn(console, 'error'); - var inst = createStubInstance(); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.registerInstance(inst); - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - }); - - it('should warn about anything than is not an internal instance', function() { - class Foo extends React.Component { - render() { - return
; - } - } - - spyOn(console, 'error'); - var warningCount = 0; - var div = document.createElement('div'); - var publicInst = ReactDOM.render(, div); - - [false, null, undefined, {}, div, publicInst].forEach(falsyValue => { - ReactDebugInstanceMap.registerInstance(falsyValue); - warningCount++; - expect(ReactDebugInstanceMap.getIDForInstance(falsyValue)).toBe(null); - warningCount++; - expect(ReactDebugInstanceMap.isRegisteredInstance(falsyValue)).toBe(false); - warningCount++; - ReactDebugInstanceMap.unregisterInstance(falsyValue); - warningCount++; - }); - - expect(console.error.argsForCall.length).toBe(warningCount); - for (var i = 0; i < warningCount.length; i++) { - // Ideally we could check for the more detailed error message here - // but it depends on the input type and is meant for internal bugs - // anyway so I don't think it's worth complicating the test with it. - expect(console.error.argsForCall[i][0]).toContain( - 'There is an internal error in the React developer tools integration.' - ); - } - }); -}); diff --git a/src/isomorphic/children/ReactChildren.js b/src/isomorphic/children/ReactChildren.js index da8848d22..645c361ff 100644 --- a/src/isomorphic/children/ReactChildren.js +++ b/src/isomorphic/children/ReactChildren.js @@ -22,3 +22,2 @@ var fourArgumentPooler = PooledClass.fourArgumentPooler; - var userProvidedKeyEscapeRegex = /\/+/g; @@ -28,3 +27,2 @@ function escapeUserProvidedKey(text) { - /** @@ -57,2 +55,4 @@ function forEachSingleChild(bookKeeping, child, name) { * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.foreach + * * The provided forEachFunc(child, index) will be called for each @@ -68,4 +68,6 @@ function forEachChildren(children, forEachFunc, forEachContext) { } - var traverseContext = - ForEachBookKeeping.getPooled(forEachFunc, forEachContext); + var traverseContext = ForEachBookKeeping.getPooled( + forEachFunc, + forEachContext, + ); traverseAllChildren(children, forEachSingleChild, traverseContext); @@ -74,3 +76,2 @@ function forEachChildren(children, forEachFunc, forEachContext) { - /** @@ -109,3 +110,3 @@ function mapSingleChildIntoContext(bookKeeping, child, childKey) { childKey, - emptyFunction.thatReturnsArgument + emptyFunction.thatReturnsArgument, ); @@ -118,8 +119,6 @@ function mapSingleChildIntoContext(bookKeeping, child, childKey) { keyPrefix + - ( - (mappedChild.key && (!child || (child.key !== mappedChild.key))) ? - escapeUserProvidedKey(mappedChild.key) + '/' : - '' - ) + - childKey + (mappedChild.key && (!child || child.key !== mappedChild.key) + ? escapeUserProvidedKey(mappedChild.key) + '/' + : '') + + childKey, ); @@ -139,3 +138,3 @@ function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { func, - context + context, ); @@ -148,2 +147,4 @@ function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.map + * * The provided mapFunction(child, key, index) will be called for each @@ -165,4 +166,2 @@ function mapChildren(children, func, context) { - - function forEachSingleChildDummy(traverseContext, child, name) { @@ -175,2 +174,4 @@ function forEachSingleChildDummy(traverseContext, child, name) { * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.count + * * @param {?*} children Children tree container. @@ -182,3 +183,2 @@ function countChildren(children, context) { - /** @@ -186,2 +186,4 @@ function countChildren(children, context) { * return an array with appropriately re-keyed children. + * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.toarray */ @@ -193,3 +195,3 @@ function toArray(children) { null, - emptyFunction.thatReturnsArgument + emptyFunction.thatReturnsArgument, ); @@ -198,3 +200,2 @@ function toArray(children) { - var ReactChildren = { diff --git a/src/isomorphic/children/__tests__/ReactChildren-test.js b/src/isomorphic/children/__tests__/ReactChildren-test.js index e638c68c1..d6e6a8bfc 100644 --- a/src/isomorphic/children/__tests__/ReactChildren-test.js +++ b/src/isomorphic/children/__tests__/ReactChildren-test.js @@ -13,3 +13,3 @@ -describe('ReactChildren', function() { +describe('ReactChildren', () => { var ReactChildren; @@ -18,3 +18,3 @@ describe('ReactChildren', function() { - beforeEach(function() { + beforeEach(() => { ReactChildren = require('ReactChildren'); @@ -24,4 +24,4 @@ describe('ReactChildren', function() { - it('should support identity for simple', function() { - var callback = jasmine.createSpy().andCallFake(function(kid, index) { + it('should support identity for simple', () => { + var callback = jasmine.createSpy().and.callFake(function(kid, index) { return kid; @@ -37,3 +37,3 @@ describe('ReactChildren', function() { expect(callback).toHaveBeenCalledWith(simpleKid, 0); - callback.reset(); + callback.calls.reset(); var mappedChildren = ReactChildren.map(instance.props.children, callback); @@ -43,4 +43,4 @@ describe('ReactChildren', function() { - it('should treat single arrayless child as being in array', function() { - var callback = jasmine.createSpy().andCallFake(function(kid, index) { + it('should treat single arrayless child as being in array', () => { + var callback = jasmine.createSpy().and.callFake(function(kid, index) { return kid; @@ -52,3 +52,3 @@ describe('ReactChildren', function() { expect(callback).toHaveBeenCalledWith(simpleKid, 0); - callback.reset(); + callback.calls.reset(); var mappedChildren = ReactChildren.map(instance.props.children, callback); @@ -58,4 +58,4 @@ describe('ReactChildren', function() { - it('should treat single child in array as expected', function() { - var callback = jasmine.createSpy().andCallFake(function(kid, index) { + it('should treat single child in array as expected', () => { + var callback = jasmine.createSpy().and.callFake(function(kid, index) { return kid; @@ -67,3 +67,3 @@ describe('ReactChildren', function() { expect(callback).toHaveBeenCalledWith(simpleKid, 0); - callback.reset(); + callback.calls.reset(); var mappedChildren = ReactChildren.map(instance.props.children, callback); @@ -71,6 +71,5 @@ describe('ReactChildren', function() { expect(mappedChildren[0]).toEqual(); - }); - it('should pass key to returned component', function() { + it('should pass key to returned component', () => { var mapFn = function(kid, index) { @@ -90,3 +89,3 @@ describe('ReactChildren', function() { - it('should invoke callback with the right context', function() { + it('should invoke callback with the right context', () => { var lastContext; @@ -105,4 +104,7 @@ describe('ReactChildren', function() { - var mappedChildren = - ReactChildren.map(instance.props.children, callback, scopeTester); + var mappedChildren = ReactChildren.map( + instance.props.children, + callback, + scopeTester, + ); @@ -112,3 +114,3 @@ describe('ReactChildren', function() { - it('should be called for each child', function() { + it('should be called for each child', () => { var zero =
; @@ -120,5 +122,5 @@ describe('ReactChildren', function() { var mapped = [ -
, // Key should be joined to obj key - null, // Key should be added even if we don't supply it! -
, // Key should be added even if not supplied! +
, // Key should be joined to obj key + null, // Key should be added even if we don't supply it! +
, // Key should be added even if not supplied! , // Map from null to something. @@ -126,3 +128,3 @@ describe('ReactChildren', function() { ]; - var callback = jasmine.createSpy().andCallFake(function(kid, index) { + var callback = jasmine.createSpy().and.callFake(function(kid, index) { return mapped[index]; @@ -146,7 +148,6 @@ describe('ReactChildren', function() { expect(callback).toHaveBeenCalledWith(four, 4); - callback.reset(); + callback.calls.reset(); - var mappedChildren = - ReactChildren.map(instance.props.children, callback); - expect(callback.calls.length).toBe(5); + var mappedChildren = ReactChildren.map(instance.props.children, callback); + expect(callback.calls.count()).toBe(5); expect(ReactChildren.count(mappedChildren)).toBe(4); @@ -158,5 +159,3 @@ describe('ReactChildren', function() { mappedChildren[3].key, - ]).toEqual( - ['giraffe/.$keyZero', '.$keyTwo', '.3', '.$keyFour'] - ); + ]).toEqual(['giraffe/.$keyZero', '.$keyTwo', '.3', '.$keyFour']); @@ -174,3 +173,3 @@ describe('ReactChildren', function() { - it('should be called for each child in nested structure', function() { + it('should be called for each child in nested structure', () => { var zero =
; @@ -187,4 +186,4 @@ describe('ReactChildren', function() { - var zeroMapped =
; // Key should be overridden - var twoMapped =
; // Key should be added even if not supplied! + var zeroMapped =
; // Key should be overridden + var twoMapped =
; // Key should be added even if not supplied! var fourMapped =
; @@ -192,6 +191,6 @@ describe('ReactChildren', function() { - var callback = jasmine.createSpy().andCallFake(function(kid, index) { - return index === 0 ? zeroMapped : - index === 1 ? twoMapped : - index === 2 ? fourMapped : fiveMapped; + var callback = jasmine.createSpy().and.callFake(function(kid, index) { + return index === 0 + ? zeroMapped + : index === 1 ? twoMapped : index === 2 ? fourMapped : fiveMapped; }); @@ -205,8 +204,3 @@ describe('ReactChildren', function() { - expect([ - frag[0].key, - frag[1].key, - frag[2].key, - frag[3].key, - ]).toEqual([ + expect([frag[0].key, frag[1].key, frag[2].key, frag[3].key]).toEqual([ 'firstHalfKey/.$keyZero', @@ -218,3 +212,3 @@ describe('ReactChildren', function() { ReactChildren.forEach(instance.props.children, callback); - expect(callback.calls.length).toBe(4); + expect(callback.calls.count()).toBe(4); expect(callback).toHaveBeenCalledWith(frag[0], 0); @@ -223,6 +217,6 @@ describe('ReactChildren', function() { expect(callback).toHaveBeenCalledWith(frag[3], 3); - callback.reset(); + callback.calls.reset(); var mappedChildren = ReactChildren.map(instance.props.children, callback); - expect(callback.calls.length).toBe(4); + expect(callback.calls.count()).toBe(4); expect(callback).toHaveBeenCalledWith(frag[0], 0); @@ -246,5 +240,9 @@ describe('ReactChildren', function() { - expect(mappedChildren[0]).toEqual(
); + expect(mappedChildren[0]).toEqual( +
, + ); expect(mappedChildren[1]).toEqual(
); - expect(mappedChildren[2]).toEqual(
); + expect(mappedChildren[2]).toEqual( +
, + ); expect(mappedChildren[3]).toEqual(
); @@ -252,3 +250,3 @@ describe('ReactChildren', function() { - it('should retain key across two mappings', function() { + it('should retain key across two mappings', () => { var zeroForceKey =
; @@ -273,5 +271,7 @@ describe('ReactChildren', function() { var expectedForcedKeys = ['giraffe/.$keyZero', '.$keyOne']; - var mappedChildrenForcedKeys = - ReactChildren.map(forcedKeys.props.children, mapFn); - var mappedForcedKeys = mappedChildrenForcedKeys.map((c) => c.key); + var mappedChildrenForcedKeys = ReactChildren.map( + forcedKeys.props.children, + mapFn, + ); + var mappedForcedKeys = mappedChildrenForcedKeys.map(c => c.key); expect(mappedForcedKeys).toEqual(expectedForcedKeys); @@ -282,11 +282,12 @@ describe('ReactChildren', function() { ]; - var remappedChildrenForcedKeys = - ReactChildren.map(mappedChildrenForcedKeys, mapFn); - expect( - remappedChildrenForcedKeys.map((c) => c.key) - ).toEqual(expectedRemappedForcedKeys); - + var remappedChildrenForcedKeys = ReactChildren.map( + mappedChildrenForcedKeys, + mapFn, + ); + expect(remappedChildrenForcedKeys.map(c => c.key)).toEqual( + expectedRemappedForcedKeys, + ); }); - it('should not throw if key provided is a dupe with array key', function() { + it('should not throw if key provided is a dupe with array key', () => { var zero =
; @@ -310,3 +311,3 @@ describe('ReactChildren', function() { - it('should use the same key for a cloned element', function() { + it('should use the same key for a cloned element', () => { var instance = ( @@ -317,10 +318,6 @@ describe('ReactChildren', function() { - var mapped = ReactChildren.map( - instance.props.children, - element => element, - ); + var mapped = ReactChildren.map(instance.props.children, element => element); - var mappedWithClone = ReactChildren.map( - instance.props.children, - element => React.cloneElement(element), + var mappedWithClone = ReactChildren.map(instance.props.children, element => + React.cloneElement(element), ); @@ -330,3 +327,3 @@ describe('ReactChildren', function() { - it('should use the same key for a cloned element with key', function() { + it('should use the same key for a cloned element with key', () => { var instance = ( @@ -337,10 +334,6 @@ describe('ReactChildren', function() { - var mapped = ReactChildren.map( - instance.props.children, - element => element, - ); + var mapped = ReactChildren.map(instance.props.children, element => element); - var mappedWithClone = ReactChildren.map( - instance.props.children, - element => React.cloneElement(element, {key: 'unique'}), + var mappedWithClone = ReactChildren.map(instance.props.children, element => + React.cloneElement(element, {key: 'unique'}), ); @@ -350,3 +343,3 @@ describe('ReactChildren', function() { - it('should return 0 for null children', function() { + it('should return 0 for null children', () => { var numberOfChildren = ReactChildren.count(null); @@ -355,3 +348,3 @@ describe('ReactChildren', function() { - it('should return 0 for undefined children', function() { + it('should return 0 for undefined children', () => { var numberOfChildren = ReactChildren.count(undefined); @@ -360,3 +353,3 @@ describe('ReactChildren', function() { - it('should return 1 for single child', function() { + it('should return 1 for single child', () => { var simpleKid = ; @@ -367,3 +360,3 @@ describe('ReactChildren', function() { - it('should count the number of children in flat structure', function() { + it('should count the number of children in flat structure', () => { var zero =
; @@ -387,3 +380,3 @@ describe('ReactChildren', function() { - it('should count the number of children in nested structure', function() { + it('should count the number of children in nested structure', () => { var zero =
; @@ -401,4 +394,4 @@ describe('ReactChildren', function() { var instance = ( -
{ - [ +
+ {[ ReactFragment.create({ @@ -409,4 +402,4 @@ describe('ReactChildren', function() { null, - ] - }
+ ]} +
); @@ -416,3 +409,3 @@ describe('ReactChildren', function() { - it('should flatten children to an array', function() { + it('should flatten children to an array', () => { expect(ReactChildren.toArray(undefined)).toEqual([]); @@ -422,6 +415,4 @@ describe('ReactChildren', function() { expect(ReactChildren.toArray([
]).length).toBe(1); - expect( - ReactChildren.toArray(
)[0].key - ).toBe( - ReactChildren.toArray([
])[0].key + expect(ReactChildren.toArray(
)[0].key).toBe( + ReactChildren.toArray([
])[0].key, ); @@ -449,7 +440,7 @@ describe('ReactChildren', function() { // null/undefined/bool are all omitted - expect(ReactChildren.toArray([1, 'two', null, undefined, true])).toEqual( - [1, 'two'] - ); + expect(ReactChildren.toArray([1, 'two', null, undefined, true])).toEqual([ + 1, + 'two', + ]); }); - }); diff --git a/src/isomorphic/children/__tests__/onlyChild-test.js b/src/isomorphic/children/__tests__/onlyChild-test.js index 9be5a5975..0c8870c6d 100644 --- a/src/isomorphic/children/__tests__/onlyChild-test.js +++ b/src/isomorphic/children/__tests__/onlyChild-test.js @@ -13,4 +13,3 @@ -describe('onlyChild', function() { - +describe('onlyChild', () => { var React; @@ -20,3 +19,3 @@ describe('onlyChild', function() { - beforeEach(function() { + beforeEach(() => { React = require('React'); @@ -24,4 +23,4 @@ describe('onlyChild', function() { onlyChild = require('onlyChild'); - WrapComponent = React.createClass({ - render: function() { + WrapComponent = class extends React.Component { + render() { return ( @@ -31,9 +30,9 @@ describe('onlyChild', function() { ); - }, - }); + } + }; }); - it('should fail when passed two children', function() { + it('should fail when passed two children', () => { expect(function() { - var instance = + var instance = ( @@ -41,3 +40,4 @@ describe('onlyChild', function() { - ; + + ); onlyChild(instance.props.children); @@ -46,8 +46,9 @@ describe('onlyChild', function() { - it('should fail when passed nully values', function() { + it('should fail when passed nully values', () => { expect(function() { - var instance = + var instance = ( {null} - ; + + ); onlyChild(instance.props.children); @@ -56,6 +57,7 @@ describe('onlyChild', function() { expect(function() { - var instance = + var instance = ( {undefined} - ; + + ); onlyChild(instance.props.children); @@ -64,8 +66,9 @@ describe('onlyChild', function() { - it('should fail when key/value objects', function() { + it('should fail when key/value objects', () => { expect(function() { - var instance = + var instance = ( {ReactFragment.create({oneThing: })} - ; + + ); onlyChild(instance.props.children); @@ -74,9 +77,9 @@ describe('onlyChild', function() { - - it('should not fail when passed interpolated single child', function() { + it('should not fail when passed interpolated single child', () => { expect(function() { - var instance = + var instance = ( {} - ; + + ); onlyChild(instance.props.children); @@ -85,9 +88,9 @@ describe('onlyChild', function() { - - it('should return the only child', function() { + it('should return the only child', () => { expect(function() { - var instance = + var instance = ( - ; + + ); onlyChild(instance.props.children); @@ -95,3 +98,2 @@ describe('onlyChild', function() { }); - }); diff --git a/src/isomorphic/children/__tests__/sliceChildren-test.js b/src/isomorphic/children/__tests__/sliceChildren-test.js index e6d448162..d255324a7 100644 --- a/src/isomorphic/children/__tests__/sliceChildren-test.js +++ b/src/isomorphic/children/__tests__/sliceChildren-test.js @@ -13,4 +13,3 @@ -describe('sliceChildren', function() { - +describe('sliceChildren', () => { var React; @@ -19,3 +18,3 @@ describe('sliceChildren', function() { - beforeEach(function() { + beforeEach(() => { React = require('React'); @@ -25,8 +24,4 @@ describe('sliceChildren', function() { - it('should render the whole set if start zero is supplied', function() { - var fullSet = [ -
, -
, -
, - ]; + it('should render the whole set if start zero is supplied', () => { + var fullSet = [
,
,
]; var children = sliceChildren(fullSet, 0); @@ -39,16 +34,9 @@ describe('sliceChildren', function() { - it('should render the remaining set if no end index is supplied', function() { - var fullSet = [ -
, -
, -
, - ]; + it('should render the remaining set if no end index is supplied', () => { + var fullSet = [
,
,
]; var children = sliceChildren(fullSet, 1); - expect(children).toEqual([ -
, -
, - ]); + expect(children).toEqual([
,
]); }); - it('should exclude everything at or after the end index', function() { + it('should exclude everything at or after the end index', () => { var fullSet = [ @@ -60,8 +48,6 @@ describe('sliceChildren', function() { var children = sliceChildren(fullSet, 1, 2); - expect(children).toEqual([ -
, - ]); + expect(children).toEqual([
]); }); - it('should allow static children to be sliced', function() { + it('should allow static children to be sliced', () => { var a = ; @@ -72,14 +58,9 @@ describe('sliceChildren', function() { var children = sliceChildren(el.props.children, 1, 2); - expect(children).toEqual([ - , - ]); + expect(children).toEqual([]); }); - it('should slice nested children', function() { + it('should slice nested children', () => { var fullSet = [
, - [ -
, -
, - ], + [
,
],
, @@ -87,7 +68,4 @@ describe('sliceChildren', function() { var children = sliceChildren(fullSet, 1, 2); - expect(children).toEqual([ -
, - ]); + expect(children).toEqual([
]); }); - }); diff --git a/src/isomorphic/children/onlyChild.js b/src/isomorphic/children/onlyChild.js index 0a33ee180..8801fd6dc 100644 --- a/src/isomorphic/children/onlyChild.js +++ b/src/isomorphic/children/onlyChild.js @@ -18,9 +18,12 @@ var invariant = require('invariant'); * Returns the first child in a collection of children and verifies that there - * is only one child in the collection. The current implementation of this - * function assumes that a single child gets passed without a wrapper, but the - * purpose of this helper function is to abstract away the particular structure - * of children. + * is only one child in the collection. + * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.only + * + * The current implementation of this function assumes that a single child gets + * passed without a wrapper, but the purpose of this helper function is to + * abstract away the particular structure of children. * * @param {?object} children Child collection structure. - * @return {ReactComponent} The first and only `ReactComponent` contained in the + * @return {ReactElement} The first and only `ReactElement` contained in the * structure. @@ -30,3 +33,3 @@ function onlyChild(children) { ReactElement.isValidElement(children), - 'onlyChild must be passed a children with exactly one child.' + 'React.Children.only expected to receive a single React element child.', ); diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js index cc09080f0..29a9ac891 100644 --- a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js +++ b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js @@ -19,2 +19,3 @@ +var PropTypes; var React; @@ -25,4 +26,8 @@ var reactComponentExpect; -describe('ReactContextValidator', function() { - beforeEach(function() { +describe('ReactContextValidator', () => { + function normalizeCodeLocInfo(str) { + return str.replace(/\(at .+?:\d+\)/g, '(at **)'); + } + + beforeEach(() => { jest.resetModuleRegistry(); @@ -32,2 +37,3 @@ describe('ReactContextValidator', function() { ReactTestUtils = require('ReactTestUtils'); + PropTypes = require('prop-types'); reactComponentExpect = require('reactComponentExpect'); @@ -38,20 +44,14 @@ describe('ReactContextValidator', function() { - it('should filter out context not in contextTypes', function() { - var Component = React.createClass({ - contextTypes: { - foo: React.PropTypes.string, - }, - - render: function() { + it('should filter out context not in contextTypes', () => { + class Component extends React.Component { + render() { return
; - }, - }); - - var ComponentInFooBarContext = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string, - bar: React.PropTypes.number, - }, - - getChildContext: function() { + } + } + Component.contextTypes = { + foo: PropTypes.string, + }; + + class ComponentInFooBarContext extends React.Component { + getChildContext() { return { @@ -60,14 +60,22 @@ describe('ReactContextValidator', function() { }; - }, + } - render: function() { + render() { return ; - }, - }); - - var instance = ReactTestUtils.renderIntoDocument(); - reactComponentExpect(instance).expectRenderedChild().scalarContextEqual({foo: 'abc'}); + } + } + ComponentInFooBarContext.childContextTypes = { + foo: PropTypes.string, + bar: PropTypes.number, + }; + + var instance = ReactTestUtils.renderIntoDocument( + , + ); + reactComponentExpect(instance) + .expectRenderedChild() + .scalarContextEqual({foo: 'abc'}); }); - it('should filter context properly in callbacks', function() { + it('should filter context properly in callbacks', () => { var actualComponentWillReceiveProps; @@ -77,9 +85,4 @@ describe('ReactContextValidator', function() { - var Parent = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string.isRequired, - bar: React.PropTypes.string.isRequired, - }, - - getChildContext: function() { + class Parent extends React.Component { + getChildContext() { return { @@ -88,36 +91,39 @@ describe('ReactContextValidator', function() { }; - }, + } - render: function() { + render() { return ; - }, - }); - - var Component = React.createClass({ - contextTypes: { - foo: React.PropTypes.string, - }, - - componentWillReceiveProps: function(nextProps, nextContext) { + } + } + Parent.childContextTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.string.isRequired, + }; + + class Component extends React.Component { + componentWillReceiveProps(nextProps, nextContext) { actualComponentWillReceiveProps = nextContext; return true; - }, + } - shouldComponentUpdate: function(nextProps, nextState, nextContext) { + shouldComponentUpdate(nextProps, nextState, nextContext) { actualShouldComponentUpdate = nextContext; return true; - }, + } - componentWillUpdate: function(nextProps, nextState, nextContext) { + componentWillUpdate(nextProps, nextState, nextContext) { actualComponentWillUpdate = nextContext; - }, + } - componentDidUpdate: function(prevProps, prevState, prevContext) { + componentDidUpdate(prevProps, prevState, prevContext) { actualComponentDidUpdate = prevContext; - }, + } - render: function() { + render() { return
; - }, - }); + } + } + Component.contextTypes = { + foo: PropTypes.string, + }; @@ -132,14 +138,13 @@ describe('ReactContextValidator', function() { - it('should check context types', function() { + it('should check context types', () => { spyOn(console, 'error'); - var Component = React.createClass({ - contextTypes: { - foo: React.PropTypes.string.isRequired, - }, - - render: function() { + class Component extends React.Component { + render() { return
; - }, - }); + } + } + Component.contextTypes = { + foo: PropTypes.string.isRequired, + }; @@ -147,14 +152,12 @@ describe('ReactContextValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Required context `foo` was not specified in `Component`.' + expect(console.error.calls.count()).toBe(1); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( + 'Warning: Failed context type: ' + + 'The context `foo` is marked as required in `Component`, but its value ' + + 'is `undefined`.\n' + + ' in Component (at **)', ); - var ComponentInFooStringContext = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string, - }, - - getChildContext: function() { + class ComponentInFooStringContext extends React.Component { + getChildContext() { return { @@ -162,11 +165,14 @@ describe('ReactContextValidator', function() { }; - }, + } - render: function() { + render() { return ; - }, - }); + } + } + ComponentInFooStringContext.childContextTypes = { + foo: PropTypes.string, + }; ReactTestUtils.renderIntoDocument( - + , ); @@ -174,10 +180,6 @@ describe('ReactContextValidator', function() { // Previous call should not error - expect(console.error.argsForCall.length).toBe(1); - - var ComponentInFooNumberContext = React.createClass({ - childContextTypes: { - foo: React.PropTypes.number, - }, + expect(console.error.calls.count()).toBe(1); - getChildContext: function() { + class ComponentInFooNumberContext extends React.Component { + getChildContext() { return { @@ -185,17 +187,23 @@ describe('ReactContextValidator', function() { }; - }, + } - render: function() { + render() { return ; - }, - }); + } + } + ComponentInFooNumberContext.childContextTypes = { + foo: PropTypes.number, + }; - ReactTestUtils.renderIntoDocument(); + ReactTestUtils.renderIntoDocument( + , + ); - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Invalid context `foo` of type `number` supplied ' + - 'to `Component`, expected `string`.' + - ' Check the render method of `ComponentInFooNumberContext`.' + expect(console.error.calls.count()).toBe(2); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( + 'Warning: Failed context type: ' + + 'Invalid context `foo` of type `number` supplied ' + + 'to `Component`, expected `string`.\n' + + ' in Component (at **)\n' + + ' in ComponentInFooNumberContext (at **)', ); @@ -203,25 +211,26 @@ describe('ReactContextValidator', function() { - it('should check child context types', function() { + it('should check child context types', () => { spyOn(console, 'error'); - var Component = React.createClass({ - childContextTypes: { - foo: React.PropTypes.string.isRequired, - bar: React.PropTypes.number, - }, - - getChildContext: function() { + class Component extends React.Component { + getChildContext() { return this.props.testContext; - }, + } - render: function() { + render() { return
; - }, - }); + } + } + Component.childContextTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number, + }; ReactTestUtils.renderIntoDocument(); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Required child context `foo` was not specified in `Component`.' + expect(console.error.calls.count()).toBe(1); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( + 'Warning: Failed child context type: ' + + 'The child context `foo` is marked as required in `Component`, but its ' + + 'value is `undefined`.\n' + + ' in Component (at **)', ); @@ -230,7 +239,8 @@ describe('ReactContextValidator', function() { - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Invalid child context `foo` of type `number` ' + - 'supplied to `Component`, expected `string`.' + expect(console.error.calls.count()).toBe(2); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( + 'Warning: Failed child context type: ' + + 'Invalid child context `foo` of type `number` ' + + 'supplied to `Component`, expected `string`.\n' + + ' in Component (at **)', ); @@ -238,13 +248,10 @@ describe('ReactContextValidator', function() { ReactTestUtils.renderIntoDocument( - + , ); - ReactTestUtils.renderIntoDocument( - - ); + ReactTestUtils.renderIntoDocument(); // Previous calls should not log errors - expect(console.error.argsForCall.length).toBe(2); + expect(console.error.calls.count()).toBe(2); }); - }); diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js deleted file mode 100644 index 56f479a42..000000000 --- a/src/isomorphic/classic/class/ReactClass.js +++ /dev/null @@ -1,862 +0,0 @@ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactClass - */ - -'use strict'; - -var ReactComponent = require('ReactComponent'); -var ReactElement = require('ReactElement'); -var ReactPropTypeLocations = require('ReactPropTypeLocations'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); -var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); - -var emptyObject = require('emptyObject'); -var invariant = require('invariant'); -var keyMirror = require('keyMirror'); -var keyOf = require('keyOf'); -var warning = require('warning'); - -var MIXINS_KEY = keyOf({mixins: null}); - -/** - * Policies that describe methods in `ReactClassInterface`. - */ -var SpecPolicy = keyMirror({ - /** - * These methods may be defined only once by the class specification or mixin. - */ - DEFINE_ONCE: null, - /** - * These methods may be defined by both the class specification and mixins. - * Subsequent definitions will be chained. These methods must return void. - */ - DEFINE_MANY: null, - /** - * These methods are overriding the base class. - */ - OVERRIDE_BASE: null, - /** - * These methods are similar to DEFINE_MANY, except we assume they return - * objects. We try to merge the keys of the return values of all the mixed in - * functions. If there is a key conflict we throw. - */ - DEFINE_MANY_MERGED: null, -}); - - -var injectedMixins = []; - -/** - * Composite components are higher-level components that compose other composite - * or native components. - * - * To create a new type of `ReactClass`, pass a specification of - * your new class to `React.createClass`. The only requirement of your class - * specification is that you implement a `render` method. - * - * var MyComponent = React.createClass({ - * render: function() { - * return
Hello World
; - * } - * }); - * - * The class specification supports a specific protocol of methods that have - * special meaning (e.g. `render`). See `ReactClassInterface` for - * more the comprehensive protocol. Any other properties and methods in the - * class specification will be available on the prototype. - * - * @interface ReactClassInterface - * @internal - */ -var ReactClassInterface = { - - /** - * An array of Mixin objects to include when defining your component. - * - * @type {array} - * @optional - */ - mixins: SpecPolicy.DEFINE_MANY, - - /** - * An object containing properties and methods that should be defined on - * the component's constructor instead of its prototype (static methods). - * - * @type {object} - * @optional - */ - statics: SpecPolicy.DEFINE_MANY, - - /** - * Definition of prop types for this component. - * - * @type {object} - * @optional - */ - propTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types for this component. - * - * @type {object} - * @optional - */ - contextTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types this component sets for its children. - * - * @type {object} - * @optional - */ - childContextTypes: SpecPolicy.DEFINE_MANY, - - // ==== Definition methods ==== - - /** - * Invoked when the component is mounted. Values in the mapping will be set on - * `this.props` if that prop is not specified (i.e. using an `in` check). - * - * This method is invoked before `getInitialState` and therefore cannot rely - * on `this.state` or use `this.setState`. - * - * @return {object} - * @optional - */ - getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Invoked once before the component is mounted. The return value will be used - * as the initial value of `this.state`. - * - * getInitialState: function() { - * return { - * isOn: false, - * fooBaz: new BazFoo() - * } - * } - * - * @return {object} - * @optional - */ - getInitialState: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * @return {object} - * @optional - */ - getChildContext: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Uses props from `this.props` and state from `this.state` to render the - * structure of the component. - * - * No guarantees are made about when or how often this method is invoked, so - * it must not have side effects. - * - * render: function() { - * var name = this.props.name; - * return
Hello, {name}!
; - * } - * - * @return {ReactComponent} - * @nosideeffects - * @required - */ - render: SpecPolicy.DEFINE_ONCE, - - - - // ==== Delegate methods ==== - - /** - * Invoked when the component is initially created and about to be mounted. - * This may have side effects, but any external subscriptions or data created - * by this method must be cleaned up in `componentWillUnmount`. - * - * @optional - */ - componentWillMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component has been mounted and has a DOM representation. - * However, there is no guarantee that the DOM node is in the document. - * - * Use this as an opportunity to operate on the DOM when the component has - * been mounted (initialized and rendered) for the first time. - * - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked before the component receives new props. - * - * Use this as an opportunity to react to a prop transition by updating the - * state using `this.setState`. Current props are accessed via `this.props`. - * - * componentWillReceiveProps: function(nextProps, nextContext) { - * this.setState({ - * likesIncreasing: nextProps.likeCount > this.props.likeCount - * }); - * } - * - * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop - * transition may cause a state change, but the opposite is not true. If you - * need it, you are probably looking for `componentWillUpdate`. - * - * @param {object} nextProps - * @optional - */ - componentWillReceiveProps: SpecPolicy.DEFINE_MANY, - - /** - * Invoked while deciding if the component should be updated as a result of - * receiving new props, state and/or context. - * - * Use this as an opportunity to `return false` when you're certain that the - * transition to the new props/state/context will not require a component - * update. - * - * shouldComponentUpdate: function(nextProps, nextState, nextContext) { - * return !equal(nextProps, this.props) || - * !equal(nextState, this.state) || - * !equal(nextContext, this.context); - * } - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @return {boolean} True if the component should update. - * @optional - */ - shouldComponentUpdate: SpecPolicy.DEFINE_ONCE, - - /** - * Invoked when the component is about to update due to a transition from - * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` - * and `nextContext`. - * - * Use this as an opportunity to perform preparation before an update occurs. - * - * NOTE: You **cannot** use `this.setState()` in this method. - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @param {ReactReconcileTransaction} transaction - * @optional - */ - componentWillUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component's DOM representation has been updated. - * - * Use this as an opportunity to operate on the DOM when the component has - * been updated. - * - * @param {object} prevProps - * @param {?object} prevState - * @param {?object} prevContext - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component is about to be removed from its parent and have - * its DOM representation destroyed. - * - * Use this as an opportunity to deallocate any external resources. - * - * NOTE: There is no `componentDidUnmount` since your component will have been - * destroyed by that point. - * - * @optional - */ - componentWillUnmount: SpecPolicy.DEFINE_MANY, - - - - // ==== Advanced methods ==== - - /** - * Updates the component's currently mounted DOM representation. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @internal - * @overridable - */ - updateComponent: SpecPolicy.OVERRIDE_BASE, - -}; - -/** - * Mapping from class specification keys to special processing functions. - * - * Although these are declared like instance properties in the specification - * when defining classes using `React.createClass`, they are actually static - * and are accessible on the constructor instead of the prototype. Despite - * being static, they must be defined outside of the "statics" key under - * which all other static methods are defined. - */ -var RESERVED_SPEC_KEYS = { - displayName: function(Constructor, displayName) { - Constructor.displayName = displayName; - }, - mixins: function(Constructor, mixins) { - if (mixins) { - for (var i = 0; i < mixins.length; i++) { - mixSpecIntoComponent(Constructor, mixins[i]); - } - } - }, - childContextTypes: function(Constructor, childContextTypes) { - if (__DEV__) { - validateTypeDef( - Constructor, - childContextTypes, - ReactPropTypeLocations.childContext - ); - } - Constructor.childContextTypes = Object.assign( - {}, - Constructor.childContextTypes, - childContextTypes - ); - }, - contextTypes: function(Constructor, contextTypes) { - if (__DEV__) { - validateTypeDef( - Constructor, - contextTypes, - ReactPropTypeLocations.context - ); - } - Constructor.contextTypes = Object.assign( - {}, - Constructor.contextTypes, - contextTypes - ); - }, - /** - * Special case getDefaultProps which should move into statics but requires - * automatic merging. - */ - getDefaultProps: function(Constructor, getDefaultProps) { - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps = createMergedResultFunction( - Constructor.getDefaultProps, - getDefaultProps - ); - } else { - Constructor.getDefaultProps = getDefaultProps; - } - }, - propTypes: function(Constructor, propTypes) { - if (__DEV__) { - validateTypeDef( - Constructor, - propTypes, - ReactPropTypeLocations.prop - ); - } - Constructor.propTypes = Object.assign( - {}, - Constructor.propTypes, - propTypes - ); - }, - statics: function(Constructor, statics) { - mixStaticSpecIntoComponent(Constructor, statics); - }, - autobind: function() {}, // noop -}; - -function validateTypeDef(Constructor, typeDef, location) { - for (var propName in typeDef) { - if (typeDef.hasOwnProperty(propName)) { - // use a warning instead of an invariant so components - // don't show up in prod but only in __DEV__ - warning( - typeof typeDef[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually from ' + - 'React.PropTypes.', - Constructor.displayName || 'ReactClass', - ReactPropTypeLocationNames[location], - propName - ); - } - } -} - -function validateMethodOverride(isAlreadyDefined, name) { - var specPolicy = ReactClassInterface.hasOwnProperty(name) ? - ReactClassInterface[name] : - null; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactClassMixin.hasOwnProperty(name)) { - invariant( - specPolicy === SpecPolicy.OVERRIDE_BASE, - 'ReactClassInterface: You are attempting to override ' + - '`%s` from your class specification. Ensure that your method names ' + - 'do not overlap with React methods.', - name - ); - } - - // Disallow defining methods more than once unless explicitly allowed. - if (isAlreadyDefined) { - invariant( - specPolicy === SpecPolicy.DEFINE_MANY || - specPolicy === SpecPolicy.DEFINE_MANY_MERGED, - 'ReactClassInterface: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be due ' + - 'to a mixin.', - name - ); - } -} - -/** - * Mixin helper which handles policy validation and reserved - * specification keys when building React classes. - */ -function mixSpecIntoComponent(Constructor, spec) { - if (!spec) { - return; - } - - invariant( - typeof spec !== 'function', - 'ReactClass: You\'re attempting to ' + - 'use a component class or function as a mixin. Instead, just use a ' + - 'regular object.' - ); - invariant( - !ReactElement.isValidElement(spec), - 'ReactClass: You\'re attempting to ' + - 'use a component as a mixin. Instead, just use a regular object.' - ); - - var proto = Constructor.prototype; - var autoBindPairs = proto.__reactAutoBindPairs; - - // By handling mixins before any other properties, we ensure the same - // chaining order is applied to methods with DEFINE_MANY policy, whether - // mixins are listed before or after these methods in the spec. - if (spec.hasOwnProperty(MIXINS_KEY)) { - RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); - } - - for (var name in spec) { - if (!spec.hasOwnProperty(name)) { - continue; - } - - if (name === MIXINS_KEY) { - // We have already handled mixins in a special case above. - continue; - } - - var property = spec[name]; - var isAlreadyDefined = proto.hasOwnProperty(name); - validateMethodOverride(isAlreadyDefined, name); - - if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { - RESERVED_SPEC_KEYS[name](Constructor, property); - } else { - // Setup methods on prototype: - // The following member methods should not be automatically bound: - // 1. Expected ReactClass methods (in the "interface"). - // 2. Overridden methods (that were mixed in). - var isReactClassMethod = - ReactClassInterface.hasOwnProperty(name); - var isFunction = typeof property === 'function'; - var shouldAutoBind = - isFunction && - !isReactClassMethod && - !isAlreadyDefined && - spec.autobind !== false; - - if (shouldAutoBind) { - autoBindPairs.push(name, property); - proto[name] = property; - } else { - if (isAlreadyDefined) { - var specPolicy = ReactClassInterface[name]; - - // These cases should already be caught by validateMethodOverride. - invariant( - isReactClassMethod && ( - specPolicy === SpecPolicy.DEFINE_MANY_MERGED || - specPolicy === SpecPolicy.DEFINE_MANY - ), - 'ReactClass: Unexpected spec policy %s for key %s ' + - 'when mixing in component specs.', - specPolicy, - name - ); - - // For methods which are defined more than once, call the existing - // methods before calling the new property, merging if appropriate. - if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) { - proto[name] = createMergedResultFunction(proto[name], property); - } else if (specPolicy === SpecPolicy.DEFINE_MANY) { - proto[name] = createChainedFunction(proto[name], property); - } - } else { - proto[name] = property; - if (__DEV__) { - // Add verbose displayName to the function, which helps when looking - // at profiling tools. - if (typeof property === 'function' && spec.displayName) { - proto[name].displayName = spec.displayName + '_' + name; - } - } - } - } - } - } -} - -function mixStaticSpecIntoComponent(Constructor, statics) { - if (!statics) { - return; - } - for (var name in statics) { - var property = statics[name]; - if (!statics.hasOwnProperty(name)) { - continue; - } - - var isReserved = name in RESERVED_SPEC_KEYS; - invariant( - !isReserved, - 'ReactClass: You are attempting to define a reserved ' + - 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + - 'as an instance property instead; it will still be accessible on the ' + - 'constructor.', - name - ); - - var isInherited = name in Constructor; - invariant( - !isInherited, - 'ReactClass: You are attempting to define ' + - '`%s` on your component more than once. This conflict may be ' + - 'due to a mixin.', - name - ); - Constructor[name] = property; - } -} - -/** - * Merge two objects, but throw if both contain the same key. - * - * @param {object} one The first object, which is mutated. - * @param {object} two The second object - * @return {object} one after it has been mutated to contain everything in two. - */ -function mergeIntoWithNoDuplicateKeys(one, two) { - invariant( - one && two && typeof one === 'object' && typeof two === 'object', - 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' - ); - - for (var key in two) { - if (two.hasOwnProperty(key)) { - invariant( - one[key] === undefined, - 'mergeIntoWithNoDuplicateKeys(): ' + - 'Tried to merge two objects with the same key: `%s`. This conflict ' + - 'may be due to a mixin; in particular, this may be caused by two ' + - 'getInitialState() or getDefaultProps() methods returning objects ' + - 'with clashing keys.', - key - ); - one[key] = two[key]; - } - } - return one; -} - -/** - * Creates a function that invokes two functions and merges their return values. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createMergedResultFunction(one, two) { - return function mergedResult() { - var a = one.apply(this, arguments); - var b = two.apply(this, arguments); - if (a == null) { - return b; - } else if (b == null) { - return a; - } - var c = {}; - mergeIntoWithNoDuplicateKeys(c, a); - mergeIntoWithNoDuplicateKeys(c, b); - return c; - }; -} - -/** - * Creates a function that invokes two functions and ignores their return vales. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createChainedFunction(one, two) { - return function chainedFunction() { - one.apply(this, arguments); - two.apply(this, arguments); - }; -} - -/** - * Binds a method to the component. - * - * @param {object} component Component whose method is going to be bound. - * @param {function} method Method to be bound. - * @return {function} The bound method. - */ -function bindAutoBindMethod(component, method) { - var boundMethod = method.bind(component); - if (__DEV__) { - boundMethod.__reactBoundContext = component; - boundMethod.__reactBoundMethod = method; - boundMethod.__reactBoundArguments = null; - var componentName = component.constructor.displayName; - var _bind = boundMethod.bind; - boundMethod.bind = function(newThis, ...args) { - // User is trying to bind() an autobound method; we effectively will - // ignore the value of "this" that the user is trying to use, so - // let's warn. - if (newThis !== component && newThis !== null) { - warning( - false, - 'bind(): React component methods may only be bound to the ' + - 'component instance. See %s', - componentName - ); - } else if (!args.length) { - warning( - false, - 'bind(): You are binding a component method to the component. ' + - 'React does this for you automatically in a high-performance ' + - 'way, so you can safely remove this call. See %s', - componentName - ); - return boundMethod; - } - var reboundMethod = _bind.apply(boundMethod, arguments); - reboundMethod.__reactBoundContext = component; - reboundMethod.__reactBoundMethod = method; - reboundMethod.__reactBoundArguments = args; - return reboundMethod; - }; - } - return boundMethod; -} - -/** - * Binds all auto-bound methods in a component. - * - * @param {object} component Component whose method is going to be bound. - */ -function bindAutoBindMethods(component) { - var pairs = component.__reactAutoBindPairs; - for (var i = 0; i < pairs.length; i += 2) { - var autoBindKey = pairs[i]; - var method = pairs[i + 1]; - component[autoBindKey] = bindAutoBindMethod( - component, - method - ); - } -} - -/** - * Add more to the ReactClass base class. These are all legacy features and - * therefore not already part of the modern ReactComponent. - */ -var ReactClassMixin = { - - /** - * TODO: This will be deprecated because state should always keep a consistent - * type signature and the only use case for this, is to avoid that. - */ - replaceState: function(newState, callback) { - this.updater.enqueueReplaceState(this, newState); - if (callback) { - this.updater.enqueueCallback(this, callback, 'replaceState'); - } - }, - - /** - * Checks whether or not this composite component is mounted. - * @return {boolean} True if mounted, false otherwise. - * @protected - * @final - */ - isMounted: function() { - return this.updater.isMounted(this); - }, -}; - -var ReactClassComponent = function() {}; -Object.assign( - ReactClassComponent.prototype, - ReactComponent.prototype, - ReactClassMixin -); - -/** - * Module for creating composite components. - * - * @class ReactClass - */ -var ReactClass = { - - /** - * Creates a composite component class given a class specification. - * - * @param {object} spec Class specification (which must define `render`). - * @return {function} Component constructor function. - * @public - */ - createClass: function(spec) { - var Constructor = function(props, context, updater) { - // This constructor gets overridden by mocks. The argument is used - // by mocks to assert on what gets mounted. - - if (__DEV__) { - warning( - this instanceof Constructor, - 'Something is calling a React component directly. Use a factory or ' + - 'JSX instead. See: https://fb.me/react-legacyfactory' - ); - } - - // Wire up auto-binding - if (this.__reactAutoBindPairs.length) { - bindAutoBindMethods(this); - } - - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - - this.state = null; - - // ReactClasses doesn't have constructors. Instead, they use the - // getInitialState and componentWillMount methods for initialization. - - var initialState = this.getInitialState ? this.getInitialState() : null; - if (__DEV__) { - // We allow auto-mocks to proceed as if they're returning null. - if (initialState === undefined && - this.getInitialState._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - initialState = null; - } - } - invariant( - typeof initialState === 'object' && !Array.isArray(initialState), - '%s.getInitialState(): must return an object or null', - Constructor.displayName || 'ReactCompositeComponent' - ); - - this.state = initialState; - }; - Constructor.prototype = new ReactClassComponent(); - Constructor.prototype.constructor = Constructor; - Constructor.prototype.__reactAutoBindPairs = []; - - injectedMixins.forEach( - mixSpecIntoComponent.bind(null, Constructor) - ); - - mixSpecIntoComponent(Constructor, spec); - - // Initialize the defaultProps property after all mixins have been merged. - if (Constructor.getDefaultProps) { - Constructor.defaultProps = Constructor.getDefaultProps(); - } - - if (__DEV__) { - // This is a tag to indicate that the use of these method names is ok, - // since it's used with createClass. If it's not, then it's likely a - // mistake so we'll warn you to use the static property, property - // initializer or constructor respectively. - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps.isReactClassApproved = {}; - } - if (Constructor.prototype.getInitialState) { - Constructor.prototype.getInitialState.isReactClassApproved = {}; - } - } - - invariant( - Constructor.prototype.render, - 'createClass(...): Class specification must implement a `render` method.' - ); - - if (__DEV__) { - warning( - !Constructor.prototype.componentShouldUpdate, - '%s has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.', - spec.displayName || 'A component' - ); - warning( - !Constructor.prototype.componentWillRecieveProps, - '%s has a method called ' + - 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', - spec.displayName || 'A component' - ); - } - - // Reduce time spent doing lookups by setting these on the prototype. - for (var methodName in ReactClassInterface) { - if (!Constructor.prototype[methodName]) { - Constructor.prototype[methodName] = null; - } - } - - return Constructor; - }, - - injection: { - injectMixin: function(mixin) { - injectedMixins.push(mixin); - }, - }, - -}; - -module.exports = ReactClass; diff --git a/src/isomorphic/classic/class/__tests__/ReactBind-test.js b/src/isomorphic/classic/class/__tests__/ReactBind-test.js deleted file mode 100644 index 63aa641a9..000000000 --- a/src/isomorphic/classic/class/__tests__/ReactBind-test.js +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Copyright 2013-present, 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 - */ -/*global global:true*/ -'use strict'; - -var React = require('React'); -var ReactTestUtils = require('ReactTestUtils'); -var reactComponentExpect = require('reactComponentExpect'); - -// TODO: Test render and all stock methods. -describe('autobinding', function() { - - it('Holds reference to instance', function() { - - var mouseDidEnter = jest.genMockFn(); - var mouseDidLeave = jest.genMockFn(); - var mouseDidClick = jest.genMockFn(); - - var TestBindComponent = React.createClass({ - getInitialState: function() { - return {something: 'hi'}; - }, - onMouseEnter: mouseDidEnter, - onMouseLeave: mouseDidLeave, - onClick: mouseDidClick, - - // auto binding only occurs on top level functions in class defs. - badIdeas: { - badBind: function() { - void this.state.something; - }, - }, - - render: function() { - return ( -
- ); - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); - - expect(function() { - var badIdea = instance1.badIdeas.badBind; - badIdea(); - }).toThrow(); - - expect(mountedInstance1.onClick).not.toBe(mountedInstance2.onClick); - - ReactTestUtils.Simulate.click(rendered1); - expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.click(rendered2); - expect(mouseDidClick.mock.instances.length).toBe(2); - expect(mouseDidClick.mock.instances[1]).toBe(mountedInstance2); - - ReactTestUtils.Simulate.mouseOver(rendered1); - expect(mouseDidEnter.mock.instances.length).toBe(1); - expect(mouseDidEnter.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.mouseOver(rendered2); - expect(mouseDidEnter.mock.instances.length).toBe(2); - expect(mouseDidEnter.mock.instances[1]).toBe(mountedInstance2); - - ReactTestUtils.Simulate.mouseOut(rendered1); - expect(mouseDidLeave.mock.instances.length).toBe(1); - expect(mouseDidLeave.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.mouseOut(rendered2); - expect(mouseDidLeave.mock.instances.length).toBe(2); - expect(mouseDidLeave.mock.instances[1]).toBe(mountedInstance2); - }); - - it('works with mixins', function() { - var mouseDidClick = jest.genMockFn(); - - var TestMixin = { - onClick: mouseDidClick, - }; - - var TestBindComponent = React.createClass({ - mixins: [TestMixin], - - render: function() { - return
; - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - ReactTestUtils.Simulate.click(rendered1); - expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); - }); - - it('warns if you try to bind to this', function() { - spyOn(console, 'error'); - - var TestBindComponent = React.createClass({ - handleClick: function() { }, - render: function() { - return
; - }, - }); - - ReactTestUtils.renderIntoDocument(); - - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: bind(): You are binding a component method to the component. ' + - 'React does this for you automatically in a high-performance ' + - 'way, so you can safely remove this call. See TestBindComponent' - ); - }); - - it('does not warn if you pass an auto-bound method to setState', function() { - spyOn(console, 'error'); - - var TestBindComponent = React.createClass({ - getInitialState: function() { - return {foo: 1}; - }, - componentDidMount: function() { - this.setState({foo: 2}, this.handleUpdate); - }, - handleUpdate: function() { - - }, - render: function() { - return
; - }, - }); - - ReactTestUtils.renderIntoDocument(); - - expect(console.error.argsForCall.length).toBe(0); - }); - -}); diff --git a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js deleted file mode 100644 index 395e34577..000000000 --- a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright 2013-present, 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 - */ -/*global global:true*/ -'use strict'; - -var React = require('React'); -var ReactTestUtils = require('ReactTestUtils'); -var reactComponentExpect = require('reactComponentExpect'); - -// TODO: Test render and all stock methods. -describe('autobind optout', function() { - - it('should work with manual binding', function() { - - var mouseDidEnter = jest.genMockFn(); - var mouseDidLeave = jest.genMockFn(); - var mouseDidClick = jest.genMockFn(); - - var TestBindComponent = React.createClass({ - autobind: false, - getInitialState: function() { - return {something: 'hi'}; - }, - onMouseEnter: mouseDidEnter, - onMouseLeave: mouseDidLeave, - onClick: mouseDidClick, - - render: function() { - return ( -
- ); - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); - - ReactTestUtils.Simulate.click(rendered1); - expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.click(rendered2); - expect(mouseDidClick.mock.instances.length).toBe(2); - expect(mouseDidClick.mock.instances[1]).toBe(mountedInstance2); - - ReactTestUtils.Simulate.mouseOver(rendered1); - expect(mouseDidEnter.mock.instances.length).toBe(1); - expect(mouseDidEnter.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.mouseOver(rendered2); - expect(mouseDidEnter.mock.instances.length).toBe(2); - expect(mouseDidEnter.mock.instances[1]).toBe(mountedInstance2); - - ReactTestUtils.Simulate.mouseOut(rendered1); - expect(mouseDidLeave.mock.instances.length).toBe(1); - expect(mouseDidLeave.mock.instances[0]).toBe(mountedInstance1); - - ReactTestUtils.Simulate.mouseOut(rendered2); - expect(mouseDidLeave.mock.instances.length).toBe(2); - expect(mouseDidLeave.mock.instances[1]).toBe(mountedInstance2); - }); - - it('should not hold reference to instance', function() { - var mouseDidClick = function() { - void this.state.something; - }; - - var TestBindComponent = React.createClass({ - autobind: false, - getInitialState: function() { - return {something: 'hi'}; - }, - onClick: mouseDidClick, - - // auto binding only occurs on top level functions in class defs. - badIdeas: { - badBind: function() { - void this.state.something; - }, - }, - - render: function() { - return ( -
- ); - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); - - expect(function() { - var badIdea = instance1.badIdeas.badBind; - badIdea(); - }).toThrow(); - - expect(mountedInstance1.onClick).toBe(mountedInstance2.onClick); - - expect(function() { - ReactTestUtils.Simulate.click(rendered1); - }).toThrow(); - - expect(function() { - ReactTestUtils.Simulate.click(rendered2); - }).toThrow(); - }); - - it('works with mixins that have not opted out of autobinding', function() { - var mouseDidClick = jest.genMockFn(); - - var TestMixin = { - onClick: mouseDidClick, - }; - - var TestBindComponent = React.createClass({ - mixins: [TestMixin], - - render: function() { - return
; - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - ReactTestUtils.Simulate.click(rendered1); - expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); - }); - - it('works with mixins that have opted out of autobinding', function() { - var mouseDidClick = jest.genMockFn(); - - var TestMixin = { - autobind: false, - onClick: mouseDidClick, - }; - - var TestBindComponent = React.createClass({ - mixins: [TestMixin], - - render: function() { - return
; - }, - }); - - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); - - ReactTestUtils.Simulate.click(rendered1); - expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); - }); - - it('does not warn if you try to bind to this', function() { - spyOn(console, 'error'); - - var TestBindComponent = React.createClass({ - autobind: false, - handleClick: function() { }, - render: function() { - return
; - }, - }); - - ReactTestUtils.renderIntoDocument(); - - expect(console.error.argsForCall.length).toBe(0); - }); - - it('does not warn if you pass an manually bound method to setState', function() { - spyOn(console, 'error'); - - var TestBindComponent = React.createClass({ - autobind: false, - getInitialState: function() { - return {foo: 1}; - }, - componentDidMount: function() { - this.setState({foo: 2}, this.handleUpdate.bind(this)); - }, - handleUpdate: function() { - - }, - render: function() { - return
; - }, - }); - - ReactTestUtils.renderIntoDocument(); - - expect(console.error.argsForCall.length).toBe(0); - }); - -}); diff --git a/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js b/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js deleted file mode 100644 index a65c68ff0..000000000 --- a/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js +++ /dev/null @@ -1,457 +0,0 @@ -/** - * Copyright 2013-present, 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 React; -var ReactTestUtils; - -var TestComponent; -var TestComponentWithPropTypes; -var TestComponentWithReverseSpec; -var mixinPropValidator; -var componentPropValidator; - -describe('ReactClass-mixin', function() { - - beforeEach(function() { - React = require('React'); - ReactTestUtils = require('ReactTestUtils'); - mixinPropValidator = jest.genMockFn(); - componentPropValidator = jest.genMockFn(); - - var MixinA = { - propTypes: { - propA: function() {}, - }, - componentDidMount: function() { - this.props.listener('MixinA didMount'); - }, - }; - - var MixinB = { - mixins: [MixinA], - propTypes: { - propB: function() {}, - }, - componentDidMount: function() { - this.props.listener('MixinB didMount'); - }, - }; - - var MixinBWithReverseSpec = { - componentDidMount: function() { - this.props.listener('MixinBWithReverseSpec didMount'); - }, - mixins: [MixinA], - }; - - var MixinC = { - statics: { - staticC: function() {}, - }, - componentDidMount: function() { - this.props.listener('MixinC didMount'); - }, - }; - - var MixinD = { - propTypes: { - value: mixinPropValidator, - }, - }; - - TestComponent = React.createClass({ - mixins: [MixinB, MixinC, MixinD], - statics: { - staticComponent: function() {}, - }, - propTypes: { - propComponent: function() {}, - }, - componentDidMount: function() { - this.props.listener('Component didMount'); - }, - render: function() { - return
; - }, - }); - - TestComponentWithReverseSpec = React.createClass({ - render: function() { - return
; - }, - componentDidMount: function() { - this.props.listener('Component didMount'); - }, - mixins: [MixinBWithReverseSpec, MixinC, MixinD], - }); - - TestComponentWithPropTypes = React.createClass({ - mixins: [MixinD], - propTypes: { - value: componentPropValidator, - }, - render: function() { - return
; - }, - }); - }); - - it('should support merging propTypes and statics', function() { - var listener = jest.genMockFn(); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - - var instancePropTypes = instance.constructor.propTypes; - - expect('propA' in instancePropTypes).toBe(true); - expect('propB' in instancePropTypes).toBe(true); - expect('propComponent' in instancePropTypes).toBe(true); - - expect('staticC' in TestComponent).toBe(true); - expect('staticComponent' in TestComponent).toBe(true); - }); - - it('should support chaining delegate functions', function() { - var listener = jest.genMockFn(); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - - expect(listener.mock.calls).toEqual([ - ['MixinA didMount'], - ['MixinB didMount'], - ['MixinC didMount'], - ['Component didMount'], - ]); - }); - - it('should chain functions regardless of spec property order', function() { - var listener = jest.genMockFn(); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - - expect(listener.mock.calls).toEqual([ - ['MixinA didMount'], - ['MixinBWithReverseSpec didMount'], - ['MixinC didMount'], - ['Component didMount'], - ]); - }); - - it('should validate prop types via mixins', function() { - expect(TestComponent.propTypes).toBeDefined(); - expect(TestComponent.propTypes.value) - .toBe(mixinPropValidator); - }); - - it('should override mixin prop types with class prop types', function() { - // Sanity check... - expect(componentPropValidator).toNotBe(mixinPropValidator); - // Actually check... - expect(TestComponentWithPropTypes.propTypes) - .toBeDefined(); - expect(TestComponentWithPropTypes.propTypes.value) - .toNotBe(mixinPropValidator); - expect(TestComponentWithPropTypes.propTypes.value) - .toBe(componentPropValidator); - }); - - - it('should support mixins with getInitialState()', function() { - var Mixin = { - getInitialState: function() { - return {mixin: true}; - }, - }; - var Component = React.createClass({ - mixins: [Mixin], - getInitialState: function() { - return {component: true}; - }, - render: function() { - return ; - }, - }); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state.component).toBe(true); - expect(instance.state.mixin).toBe(true); - }); - - it('should throw with conflicting getInitialState() methods', function() { - var Mixin = { - getInitialState: function() { - return {x: true}; - }, - }; - var Component = React.createClass({ - mixins: [Mixin], - getInitialState: function() { - return {x: true}; - }, - render: function() { - return ; - }, - }); - var instance = ; - expect(function() { - instance = ReactTestUtils.renderIntoDocument(instance); - }).toThrow( - 'mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the ' + - 'same key: `x`. This conflict may be due to a mixin; in particular, ' + - 'this may be caused by two getInitialState() or getDefaultProps() ' + - 'methods returning objects with clashing keys.' - ); - }); - - it('should not mutate objects returned by getInitialState()', function() { - var Mixin = { - getInitialState: function() { - return Object.freeze({mixin: true}); - }, - }; - var Component = React.createClass({ - mixins: [Mixin], - getInitialState: function() { - return Object.freeze({component: true}); - }, - render: function() { - return ; - }, - }); - expect(() => { - ReactTestUtils.renderIntoDocument(); - }).not.toThrow(); - }); - - it('should support statics in mixins', function() { - var Mixin = { - statics: { - foo: 'bar', - }, - }; - var Component = React.createClass({ - mixins: [Mixin], - - statics: { - abc: 'def', - }, - - render: function() { - return ; - }, - }); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.constructor.foo).toBe('bar'); - expect(Component.foo).toBe('bar'); - expect(instance.constructor.abc).toBe('def'); - expect(Component.abc).toBe('def'); - }); - - it("should throw if mixins override each others' statics", function() { - expect(function() { - var Mixin = { - statics: { - abc: 'foo', - }, - }; - React.createClass({ - mixins: [Mixin], - - statics: { - abc: 'bar', - }, - - render: function() { - return ; - }, - }); - }).toThrow( - 'ReactClass: You are attempting to define `abc` on your component more ' + - 'than once. This conflict may be due to a mixin.' - ); - }); - - it('should throw if mixins override functions in statics', function() { - expect(function() { - var Mixin = { - statics: { - abc: function() { - console.log('foo'); - }, - }, - }; - React.createClass({ - mixins: [Mixin], - - statics: { - abc: function() { - console.log('bar'); - }, - }, - - render: function() { - return ; - }, - }); - }).toThrow( - 'ReactClass: You are attempting to define `abc` on your component ' + - 'more than once. This conflict may be due to a mixin.' - ); - }); - - it('should throw if the mixin is a React component', function() { - expect(function() { - React.createClass({ - mixins: [
], - - render: function() { - return ; - }, - }); - }).toThrow( - 'ReactClass: You\'re attempting to use a component as a mixin. ' + - 'Instead, just use a regular object.' - ); - }); - - it('should throw if the mixin is a React component class', function() { - expect(function() { - var Component = React.createClass({ - render: function() { - return ; - }, - }); - - React.createClass({ - mixins: [Component], - - render: function() { - return ; - }, - }); - }).toThrow( - 'ReactClass: You\'re attempting to use a component class or function ' + - 'as a mixin. Instead, just use a regular object.' - ); - }); - - it('should have bound the mixin methods to the component', function() { - var mixin = { - mixinFunc: function() { - return this; - }, - }; - - var Component = React.createClass({ - mixins: [mixin], - componentDidMount: function() { - expect(this.mixinFunc()).toBe(this); - }, - render: function() { - return ; - }, - }); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - }); - - it('should include the mixin keys in even if their values are falsy', function() { - var mixin = { - keyWithNullValue: null, - randomCounter: 0, - }; - - var Component = React.createClass({ - mixins: [mixin], - componentDidMount: function() { - expect(this.randomCounter).toBe(0); - expect(this.keyWithNullValue).toBeNull(); - }, - render: function() { - return ; - }, - }); - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - }); - - it('should work with a null getInitialState return value and a mixin', () => { - var Component; - var instance; - - var Mixin = { - getInitialState: function() { - return {foo: 'bar'}; - }, - }; - Component = React.createClass({ - mixins: [Mixin], - getInitialState: function() { - return null; - }, - render: function() { - return ; - }, - }); - expect( - () => ReactTestUtils.renderIntoDocument() - ).not.toThrow(); - - instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'bar'}); - - // Also the other way round should work - var Mixin2 = { - getInitialState: function() { - return null; - }, - }; - Component = React.createClass({ - mixins: [Mixin2], - getInitialState: function() { - return {foo: 'bar'}; - }, - render: function() { - return ; - }, - }); - expect( - () => ReactTestUtils.renderIntoDocument() - ).not.toThrow(); - - instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'bar'}); - - // Multiple mixins should be fine too - Component = React.createClass({ - mixins: [Mixin, Mixin2], - getInitialState: function() { - return {x: true}; - }, - render: function() { - return ; - }, - }); - expect( - () => ReactTestUtils.renderIntoDocument() - ).not.toThrow(); - - instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'bar', x: true}); - }); - -}); diff --git a/src/isomorphic/classic/class/__tests__/ReactCreateClass-test.js b/src/isomorphic/classic/class/__tests__/ReactCreateClass-test.js new file mode 100644 index 000000000..61f4fafa0 --- /dev/null +++ b/src/isomorphic/classic/class/__tests__/ReactCreateClass-test.js @@ -0,0 +1,439 @@ +/** + * Copyright 2013-present, 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 PropTypes; +var React; +var ReactDOM; +var ReactTestUtils; +var createReactClass; + +describe('ReactClass-spec', () => { + beforeEach(() => { + PropTypes = require('prop-types'); + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactTestUtils = require('ReactTestUtils'); + var createReactClassFactory = require('create-react-class/factory'); + createReactClass = createReactClassFactory( + React.Component, + React.isValidElement, + require('ReactNoopUpdateQueue'), + ); + }); + + it('should throw when `render` is not specified', () => { + expect(function() { + createReactClass({}); + }).toThrowError( + 'createClass(...): Class specification must implement a `render` method.', + ); + }); + + // TODO: Update babel-plugin-transform-react-display-name + xit('should copy `displayName` onto the Constructor', () => { + var TestComponent = createReactClass({ + render: function() { + return
; + }, + }); + + expect(TestComponent.displayName).toBe('TestComponent'); + }); + + it('should copy prop types onto the Constructor', () => { + var propValidator = jest.fn(); + var TestComponent = createReactClass({ + propTypes: { + value: propValidator, + }, + render: function() { + return
; + }, + }); + + expect(TestComponent.propTypes).toBeDefined(); + expect(TestComponent.propTypes.value).toBe(propValidator); + }); + + it('should warn on invalid prop types', () => { + spyOn(console, 'error'); + createReactClass({ + displayName: 'Component', + propTypes: { + prop: null, + }, + render: function() { + return {this.props.prop}; + }, + }); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Component: prop type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ); + }); + + it('should warn on invalid context types', () => { + spyOn(console, 'error'); + createReactClass({ + displayName: 'Component', + contextTypes: { + prop: null, + }, + render: function() { + return {this.props.prop}; + }, + }); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Component: context type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ); + }); + + it('should throw on invalid child context types', () => { + spyOn(console, 'error'); + createReactClass({ + displayName: 'Component', + childContextTypes: { + prop: null, + }, + render: function() { + return {this.props.prop}; + }, + }); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Component: child context type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ); + }); + + it('should warn when mispelling shouldComponentUpdate', () => { + spyOn(console, 'error'); + + createReactClass({ + componentShouldUpdate: function() { + return false; + }, + render: function() { + return
; + }, + }); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: A component has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ); + + createReactClass({ + displayName: 'NamedComponent', + componentShouldUpdate: function() { + return false; + }, + render: function() { + return
; + }, + }); + expect(console.error.calls.count()).toBe(2); + expect(console.error.calls.argsFor(1)[0]).toBe( + 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ); + }); + + it('should warn when mispelling componentWillReceiveProps', () => { + spyOn(console, 'error'); + createReactClass({ + componentWillRecieveProps: function() { + return false; + }, + render: function() { + return
; + }, + }); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + + 'mean componentWillReceiveProps()?', + ); + }); + + it('should throw if a reserved property is in statics', () => { + expect(function() { + createReactClass({ + statics: { + getDefaultProps: function() { + return { + foo: 0, + }; + }, + }, + + render: function() { + return ; + }, + }); + }).toThrowError( + 'ReactClass: You are attempting to define a reserved property, ' + + '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' + + 'it as an instance property instead; it will still be accessible on ' + + 'the constructor.', + ); + }); + + // TODO: Consider actually moving these to statics or drop this unit test. + + xit('should warn when using deprecated non-static spec keys', () => { + spyOn(console, 'error'); + createReactClass({ + mixins: [{}], + propTypes: { + foo: PropTypes.string, + }, + contextTypes: { + foo: PropTypes.string, + }, + childContextTypes: { + foo: PropTypes.string, + }, + render: function() { + return
; + }, + }); + expect(console.error.calls.count()).toBe(4); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'createClass(...): `mixins` is now a static property and should ' + + 'be defined inside "statics".', + ); + expect(console.error.calls.argsFor(1)[0]).toBe( + 'createClass(...): `propTypes` is now a static property and should ' + + 'be defined inside "statics".', + ); + expect(console.error.calls.argsFor(2)[0]).toBe( + 'createClass(...): `contextTypes` is now a static property and ' + + 'should be defined inside "statics".', + ); + expect(console.error.calls.argsFor(3)[0]).toBe( + 'createClass(...): `childContextTypes` is now a static property and ' + + 'should be defined inside "statics".', + ); + }); + + it('should support statics', () => { + var Component = createReactClass({ + statics: { + abc: 'def', + def: 0, + ghi: null, + jkl: 'mno', + pqr: function() { + return this; + }, + }, + + render: function() { + return ; + }, + }); + var instance = ; + instance = ReactTestUtils.renderIntoDocument(instance); + expect(instance.constructor.abc).toBe('def'); + expect(Component.abc).toBe('def'); + expect(instance.constructor.def).toBe(0); + expect(Component.def).toBe(0); + expect(instance.constructor.ghi).toBe(null); + expect(Component.ghi).toBe(null); + expect(instance.constructor.jkl).toBe('mno'); + expect(Component.jkl).toBe('mno'); + expect(instance.constructor.pqr()).toBe(Component); + expect(Component.pqr()).toBe(Component); + }); + + it('should work with object getInitialState() return values', () => { + var Component = createReactClass({ + getInitialState: function() { + return { + occupation: 'clown', + }; + }, + render: function() { + return ; + }, + }); + var instance = ; + instance = ReactTestUtils.renderIntoDocument(instance); + expect(instance.state.occupation).toEqual('clown'); + }); + + it('renders based on context getInitialState', () => { + var Foo = createReactClass({ + contextTypes: { + className: PropTypes.string, + }, + getInitialState() { + return {className: this.context.className}; + }, + render() { + return ; + }, + }); + + var Outer = createReactClass({ + childContextTypes: { + className: PropTypes.string, + }, + getChildContext() { + return {className: 'foo'}; + }, + render() { + return ; + }, + }); + + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(container.firstChild.className).toBe('foo'); + }); + + it('should throw with non-object getInitialState() return values', () => { + [['an array'], 'a string', 1234].forEach(function(state) { + var Component = createReactClass({ + getInitialState: function() { + return state; + }, + render: function() { + return ; + }, + }); + var instance = ; + expect(function() { + instance = ReactTestUtils.renderIntoDocument(instance); + }).toThrowError( + 'Component.getInitialState(): must return an object or null', + ); + }); + }); + + it('should work with a null getInitialState() return value', () => { + var Component = createReactClass({ + getInitialState: function() { + return null; + }, + render: function() { + return ; + }, + }); + expect(() => + ReactTestUtils.renderIntoDocument(), + ).not.toThrow(); + }); + + it('should throw when using legacy factories', () => { + spyOn(console, 'error'); + var Component = createReactClass({ + render() { + return
; + }, + }); + + expect(() => Component()).toThrow(); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Something is calling a React component directly. Use a ' + + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + ); + }); + + it('replaceState and callback works', () => { + var ops = []; + var Component = createReactClass({ + getInitialState() { + return {step: 0}; + }, + render() { + ops.push('Render: ' + this.state.step); + return
; + }, + }); + + var instance = ReactTestUtils.renderIntoDocument(); + instance.replaceState({step: 1}, () => { + ops.push('Callback: ' + instance.state.step); + }); + expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']); + }); + + it('isMounted works', () => { + spyOn(console, 'error'); + + var ops = []; + var instance; + var Component = createReactClass({ + displayName: 'MyComponent', + log(name) { + ops.push(`${name}: ${this.isMounted()}`); + }, + getInitialState() { + this.log('getInitialState'); + return {}; + }, + componentWillMount() { + this.log('componentWillMount'); + }, + componentDidMount() { + this.log('componentDidMount'); + }, + componentWillUpdate() { + this.log('componentWillUpdate'); + }, + componentDidUpdate() { + this.log('componentDidUpdate'); + }, + componentWillUnmount() { + this.log('componentWillUnmount'); + }, + render() { + instance = this; + this.log('render'); + return
; + }, + }); + + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + ReactDOM.unmountComponentAtNode(container); + instance.log('after unmount'); + expect(ops).toEqual([ + 'getInitialState: false', + 'componentWillMount: false', + 'render: false', + 'componentDidMount: true', + 'componentWillUpdate: true', + 'render: true', + 'componentDidUpdate: true', + 'componentWillUnmount: false', + 'after unmount: false', + ]); + + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toEqual( + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.', + ); + }); +}); diff --git a/src/isomorphic/classic/class/__tests__/ReactClass-test.js b/src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js similarity index 51% rename from src/isomorphic/classic/class/__tests__/ReactClass-test.js rename to src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js index 07a1e4bcb..4c89c732a 100644 --- a/src/isomorphic/classic/class/__tests__/ReactClass-test.js +++ b/src/isomorphic/classic/class/__tests__/create-react-class-integration-test.js @@ -16,6 +16,6 @@ var ReactDOM; var ReactTestUtils; +var createReactClass; -describe('ReactClass-spec', function() { - - beforeEach(function() { +describe('create-react-class-integration', () => { + beforeEach(() => { React = require('React'); @@ -23,9 +23,15 @@ describe('ReactClass-spec', function() { ReactTestUtils = require('ReactTestUtils'); + var createReactClassFactory = require('create-react-class/factory'); + createReactClass = createReactClassFactory( + React.Component, + React.isValidElement, + require('ReactNoopUpdateQueue'), + ); }); - it('should throw when `render` is not specified', function() { + it('should throw when `render` is not specified', () => { expect(function() { - React.createClass({}); - }).toThrow( - 'createClass(...): Class specification must implement a `render` method.' + createReactClass({}); + }).toThrowError( + 'createClass(...): Class specification must implement a `render` method.', ); @@ -33,16 +39,5 @@ describe('ReactClass-spec', function() { - it('should copy `displayName` onto the Constructor', function() { - var TestComponent = React.createClass({ - render: function() { - return
; - }, - }); - - expect(TestComponent.displayName) - .toBe('TestComponent'); - }); - - it('should copy prop types onto the Constructor', function() { - var propValidator = jest.genMockFn(); - var TestComponent = React.createClass({ + it('should copy prop types onto the Constructor', () => { + var propValidator = jest.fn(); + var TestComponent = createReactClass({ propTypes: { @@ -56,9 +51,8 @@ describe('ReactClass-spec', function() { expect(TestComponent.propTypes).toBeDefined(); - expect(TestComponent.propTypes.value) - .toBe(propValidator); + expect(TestComponent.propTypes.value).toBe(propValidator); }); - it('should warn on invalid prop types', function() { + it('should warn on invalid prop types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', @@ -71,6 +65,6 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Component: prop type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.' + 'it must be a function, usually from React.PropTypes.', ); @@ -78,5 +72,5 @@ describe('ReactClass-spec', function() { - it('should warn on invalid context types', function() { + it('should warn on invalid context types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', @@ -89,6 +83,6 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Component: context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.' + 'it must be a function, usually from React.PropTypes.', ); @@ -96,5 +90,5 @@ describe('ReactClass-spec', function() { - it('should throw on invalid child context types', function() { + it('should throw on invalid child context types', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ displayName: 'Component', @@ -107,6 +101,6 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Component: child context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.' + 'it must be a function, usually from React.PropTypes.', ); @@ -114,6 +108,6 @@ describe('ReactClass-spec', function() { - it('should warn when mispelling shouldComponentUpdate', function() { + it('should warn when mispelling shouldComponentUpdate', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ componentShouldUpdate: function() { @@ -125,10 +119,10 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: A component has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', ); - React.createClass({ + createReactClass({ displayName: 'NamedComponent', @@ -141,7 +135,7 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toBe( + expect(console.error.calls.count()).toBe(2); + expect(console.error.calls.argsFor(1)[0]).toBe( 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', ); @@ -149,5 +143,5 @@ describe('ReactClass-spec', function() { - it('should warn when mispelling componentWillReceiveProps', function() { + it('should warn when mispelling componentWillReceiveProps', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ componentWillRecieveProps: function() { @@ -159,6 +153,6 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + - 'mean componentWillReceiveProps()?' + 'mean componentWillReceiveProps()?', ); @@ -166,5 +160,5 @@ describe('ReactClass-spec', function() { - it('should throw if a reserved property is in statics', function() { + it('should throw if a reserved property is in statics', () => { expect(function() { - React.createClass({ + createReactClass({ statics: { @@ -181,7 +175,7 @@ describe('ReactClass-spec', function() { }); - }).toThrow( + }).toThrowError( 'ReactClass: You are attempting to define a reserved property, ' + - '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' + - 'it as an instance property instead; it will still be accessible on ' + - 'the constructor.' + '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' + + 'it as an instance property instead; it will still be accessible on ' + + 'the constructor.', ); @@ -191,5 +185,5 @@ describe('ReactClass-spec', function() { - xit('should warn when using deprecated non-static spec keys', function() { + xit('should warn when using deprecated non-static spec keys', () => { spyOn(console, 'error'); - React.createClass({ + createReactClass({ mixins: [{}], @@ -208,18 +202,18 @@ describe('ReactClass-spec', function() { }); - expect(console.error.argsForCall.length).toBe(4); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(4); + expect(console.error.calls.argsFor(0)[0]).toBe( 'createClass(...): `mixins` is now a static property and should ' + - 'be defined inside "statics".' + 'be defined inside "statics".', ); - expect(console.error.argsForCall[1][0]).toBe( + expect(console.error.calls.argsFor(1)[0]).toBe( 'createClass(...): `propTypes` is now a static property and should ' + - 'be defined inside "statics".' + 'be defined inside "statics".', ); - expect(console.error.argsForCall[2][0]).toBe( + expect(console.error.calls.argsFor(2)[0]).toBe( 'createClass(...): `contextTypes` is now a static property and ' + - 'should be defined inside "statics".' + 'should be defined inside "statics".', ); - expect(console.error.argsForCall[3][0]).toBe( + expect(console.error.calls.argsFor(3)[0]).toBe( 'createClass(...): `childContextTypes` is now a static property and ' + - 'should be defined inside "statics".' + 'should be defined inside "statics".', ); @@ -227,4 +221,4 @@ describe('ReactClass-spec', function() { - it('should support statics', function() { - var Component = React.createClass({ + it('should support statics', () => { + var Component = createReactClass({ statics: { @@ -257,4 +251,4 @@ describe('ReactClass-spec', function() { - it('should work with object getInitialState() return values', function() { - var Component = React.createClass({ + it('should work with object getInitialState() return values', () => { + var Component = createReactClass({ getInitialState: function() { @@ -273,4 +267,4 @@ describe('ReactClass-spec', function() { - it('renders based on context getInitialState', function() { - var Foo = React.createClass({ + it('renders based on context getInitialState', () => { + var Foo = createReactClass({ contextTypes: { @@ -286,3 +280,3 @@ describe('ReactClass-spec', function() { - var Outer = React.createClass({ + var Outer = createReactClass({ childContextTypes: { @@ -303,5 +297,5 @@ describe('ReactClass-spec', function() { - it('should throw with non-object getInitialState() return values', function() { + it('should throw with non-object getInitialState() return values', () => { [['an array'], 'a string', 1234].forEach(function(state) { - var Component = React.createClass({ + var Component = createReactClass({ getInitialState: function() { @@ -316,4 +310,4 @@ describe('ReactClass-spec', function() { instance = ReactTestUtils.renderIntoDocument(instance); - }).toThrow( - 'Component.getInitialState(): must return an object or null' + }).toThrowError( + 'Component.getInitialState(): must return an object or null', ); @@ -322,4 +316,4 @@ describe('ReactClass-spec', function() { - it('should work with a null getInitialState() return value', function() { - var Component = React.createClass({ + it('should work with a null getInitialState() return value', () => { + var Component = createReactClass({ getInitialState: function() { @@ -331,4 +325,4 @@ describe('ReactClass-spec', function() { }); - expect( - () => ReactTestUtils.renderIntoDocument() + expect(() => + ReactTestUtils.renderIntoDocument(), ).not.toThrow(); @@ -336,5 +330,5 @@ describe('ReactClass-spec', function() { - it('should throw when using legacy factories', function() { + it('should throw when using legacy factories', () => { spyOn(console, 'error'); - var Component = React.createClass({ + var Component = createReactClass({ render() { @@ -345,6 +339,6 @@ describe('ReactClass-spec', function() { expect(() => Component()).toThrow(); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Something is calling a React component directly. Use a ' + - 'factory or JSX instead. See: https://fb.me/react-legacyfactory' + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', ); @@ -352,2 +346,81 @@ describe('ReactClass-spec', function() { + it('replaceState and callback works', () => { + var ops = []; + var Component = createReactClass({ + getInitialState() { + return {step: 0}; + }, + render() { + ops.push('Render: ' + this.state.step); + return
; + }, + }); + + var instance = ReactTestUtils.renderIntoDocument(); + instance.replaceState({step: 1}, () => { + ops.push('Callback: ' + instance.state.step); + }); + expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']); + }); + + it('isMounted works', () => { + spyOn(console, 'error'); + + var ops = []; + var instance; + var Component = createReactClass({ + displayName: 'MyComponent', + log(name) { + ops.push(`${name}: ${this.isMounted()}`); + }, + getInitialState() { + this.log('getInitialState'); + return {}; + }, + componentWillMount() { + this.log('componentWillMount'); + }, + componentDidMount() { + this.log('componentDidMount'); + }, + componentWillUpdate() { + this.log('componentWillUpdate'); + }, + componentDidUpdate() { + this.log('componentDidUpdate'); + }, + componentWillUnmount() { + this.log('componentWillUnmount'); + }, + render() { + instance = this; + this.log('render'); + return
; + }, + }); + + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + ReactDOM.unmountComponentAtNode(container); + instance.log('after unmount'); + expect(ops).toEqual([ + 'getInitialState: false', + 'componentWillMount: false', + 'render: false', + 'componentDidMount: true', + 'componentWillUpdate: true', + 'render: true', + 'componentDidUpdate: true', + 'componentWillUnmount: false', + 'after unmount: false', + ]); + + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toEqual( + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.', + ); + }); }); diff --git a/src/isomorphic/classic/class/createClass.js b/src/isomorphic/classic/class/createClass.js new file mode 100644 index 000000000..3791e5392 --- /dev/null +++ b/src/isomorphic/classic/class/createClass.js @@ -0,0 +1,19 @@ +/** + * Copyright 2013-present, 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. + * + * @providesModule createClass + */ + +'use strict'; + +var {Component} = require('ReactBaseClasses'); +var {isValidElement} = require('ReactElement'); +var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); +var factory = require('create-react-class/factory'); + +module.exports = factory(Component, isValidElement, ReactNoopUpdateQueue); diff --git a/src/isomorphic/classic/element/ReactCurrentOwner.js b/src/isomorphic/classic/element/ReactCurrentOwner.js index e47505f41..a095fe009 100644 --- a/src/isomorphic/classic/element/ReactCurrentOwner.js +++ b/src/isomorphic/classic/element/ReactCurrentOwner.js @@ -9,2 +9,3 @@ * @providesModule ReactCurrentOwner + * @flow */ @@ -13,2 +14,4 @@ +import type {ReactInstance} from 'ReactInstanceType'; + /** @@ -20,3 +23,2 @@ var ReactCurrentOwner = { - /** @@ -25,4 +27,3 @@ var ReactCurrentOwner = { */ - current: null, - + current: (null: null | ReactInstance), }; diff --git a/src/isomorphic/classic/element/ReactDOMFactories.js b/src/isomorphic/classic/element/ReactDOMFactories.js deleted file mode 100644 index 422aaae77..000000000 --- a/src/isomorphic/classic/element/ReactDOMFactories.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactDOMFactories - */ - -'use strict'; - -var ReactElement = require('ReactElement'); -var ReactElementValidator = require('ReactElementValidator'); - -var mapObject = require('mapObject'); - -/** - * Create a factory that creates HTML tag elements. - * - * @param {string} tag Tag name (e.g. `div`). - * @private - */ -function createDOMFactory(tag) { - if (__DEV__) { - return ReactElementValidator.createFactory(tag); - } - return ReactElement.createFactory(tag); -} - -/** - * Creates a mapping from supported HTML tags to `ReactDOMComponent` classes. - * This is also accessible via `React.DOM`. - * - * @public - */ -var ReactDOMFactories = mapObject({ - a: 'a', - abbr: 'abbr', - address: 'address', - area: 'area', - article: 'article', - aside: 'aside', - audio: 'audio', - b: 'b', - base: 'base', - bdi: 'bdi', - bdo: 'bdo', - big: 'big', - blockquote: 'blockquote', - body: 'body', - br: 'br', - button: 'button', - canvas: 'canvas', - caption: 'caption', - cite: 'cite', - code: 'code', - col: 'col', - colgroup: 'colgroup', - data: 'data', - datalist: 'datalist', - dd: 'dd', - del: 'del', - details: 'details', - dfn: 'dfn', - dialog: 'dialog', - div: 'div', - dl: 'dl', - dt: 'dt', - em: 'em', - embed: 'embed', - fieldset: 'fieldset', - figcaption: 'figcaption', - figure: 'figure', - footer: 'footer', - form: 'form', - h1: 'h1', - h2: 'h2', - h3: 'h3', - h4: 'h4', - h5: 'h5', - h6: 'h6', - head: 'head', - header: 'header', - hgroup: 'hgroup', - hr: 'hr', - html: 'html', - i: 'i', - iframe: 'iframe', - img: 'img', - input: 'input', - ins: 'ins', - kbd: 'kbd', - keygen: 'keygen', - label: 'label', - legend: 'legend', - li: 'li', - link: 'link', - main: 'main', - map: 'map', - mark: 'mark', - menu: 'menu', - menuitem: 'menuitem', - meta: 'meta', - meter: 'meter', - nav: 'nav', - noscript: 'noscript', - object: 'object', - ol: 'ol', - optgroup: 'optgroup', - option: 'option', - output: 'output', - p: 'p', - param: 'param', - picture: 'picture', - pre: 'pre', - progress: 'progress', - q: 'q', - rp: 'rp', - rt: 'rt', - ruby: 'ruby', - s: 's', - samp: 'samp', - script: 'script', - section: 'section', - select: 'select', - small: 'small', - source: 'source', - span: 'span', - strong: 'strong', - style: 'style', - sub: 'sub', - summary: 'summary', - sup: 'sup', - table: 'table', - tbody: 'tbody', - td: 'td', - textarea: 'textarea', - tfoot: 'tfoot', - th: 'th', - thead: 'thead', - time: 'time', - title: 'title', - tr: 'tr', - track: 'track', - u: 'u', - ul: 'ul', - 'var': 'var', - video: 'video', - wbr: 'wbr', - - // SVG - circle: 'circle', - clipPath: 'clipPath', - defs: 'defs', - ellipse: 'ellipse', - g: 'g', - image: 'image', - line: 'line', - linearGradient: 'linearGradient', - mask: 'mask', - path: 'path', - pattern: 'pattern', - polygon: 'polygon', - polyline: 'polyline', - radialGradient: 'radialGradient', - rect: 'rect', - stop: 'stop', - svg: 'svg', - text: 'text', - tspan: 'tspan', - -}, createDOMFactory); - -module.exports = ReactDOMFactories; diff --git a/src/isomorphic/classic/element/ReactElement.js b/src/isomorphic/classic/element/ReactElement.js index ee46a5f39..15ff17764 100644 --- a/src/isomorphic/classic/element/ReactElement.js +++ b/src/isomorphic/classic/element/ReactElement.js @@ -17,8 +17,5 @@ var warning = require('warning'); var canDefineProperty = require('canDefineProperty'); +var hasOwnProperty = Object.prototype.hasOwnProperty; -// The Symbol used to tag the ReactElement type. If there is no native Symbol -// nor polyfill, then a plain number is used for performance. -var REACT_ELEMENT_TYPE = - (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || - 0xeac7; +var REACT_ELEMENT_TYPE = require('ReactElementSymbol'); @@ -33,2 +30,68 @@ var specialPropKeyWarningShown, specialPropRefWarningShown; +function hasValidRef(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'ref')) { + var getter = Object.getOwnPropertyDescriptor(config, 'ref').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.ref !== undefined; +} + +function hasValidKey(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'key')) { + var getter = Object.getOwnPropertyDescriptor(config, 'key').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.key !== undefined; +} + +function defineKeyPropWarningGetter(props, displayName) { + var warnAboutAccessingKey = function() { + if (!specialPropKeyWarningShown) { + specialPropKeyWarningShown = true; + warning( + false, + '%s: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + displayName, + ); + } + }; + warnAboutAccessingKey.isReactWarning = true; + Object.defineProperty(props, 'key', { + get: warnAboutAccessingKey, + configurable: true, + }); +} + +function defineRefPropWarningGetter(props, displayName) { + var warnAboutAccessingRef = function() { + if (!specialPropRefWarningShown) { + specialPropRefWarningShown = true; + warning( + false, + '%s: `ref` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + displayName, + ); + } + }; + warnAboutAccessingRef.isReactWarning = true; + Object.defineProperty(props, 'ref', { + get: warnAboutAccessingRef, + configurable: true, + }); +} + /** @@ -115,2 +178,6 @@ var ReactElement = function(type, key, ref, self, source, owner, props) { +/** + * Create and return a new ReactElement of the given type. + * See https://facebook.github.io/react/docs/top-level-api.html#react.createelement + */ ReactElement.createElement = function(type, config, children) { @@ -127,11 +194,9 @@ ReactElement.createElement = function(type, config, children) { if (config != null) { - if (__DEV__) { - ref = !config.hasOwnProperty('ref') || - Object.getOwnPropertyDescriptor(config, 'ref').get ? null : config.ref; - key = !config.hasOwnProperty('key') || - Object.getOwnPropertyDescriptor(config, 'key').get ? null : '' + config.key; - } else { - ref = config.ref === undefined ? null : config.ref; - key = config.key === undefined ? null : '' + config.key; + if (hasValidRef(config)) { + ref = config.ref; } + if (hasValidKey(config)) { + key = '' + config.key; + } + self = config.__self === undefined ? null : config.__self; @@ -140,4 +205,6 @@ ReactElement.createElement = function(type, config, children) { for (propName in config) { - if (config.hasOwnProperty(propName) && - !RESERVED_PROPS.hasOwnProperty(propName)) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { props[propName] = config[propName]; @@ -157,2 +224,7 @@ ReactElement.createElement = function(type, config, children) { } + if (__DEV__) { + if (Object.freeze) { + Object.freeze(childArray); + } + } props.children = childArray; @@ -170,43 +242,16 @@ ReactElement.createElement = function(type, config, children) { if (__DEV__) { - // Create dummy `key` and `ref` property to `props` to warn users - // against its use - if (typeof props.$$typeof === 'undefined' || - props.$$typeof !== REACT_ELEMENT_TYPE) { - if (!props.hasOwnProperty('key')) { - Object.defineProperty(props, 'key', { - get: function() { - if (!specialPropKeyWarningShown) { - specialPropKeyWarningShown = true; - warning( - false, - '%s: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://fb.me/react-special-props)', - typeof type === 'function' && 'displayName' in type ? type.displayName : 'Element' - ); - } - return undefined; - }, - configurable: true, - }); - } - if (!props.hasOwnProperty('ref')) { - Object.defineProperty(props, 'ref', { - get: function() { - if (!specialPropRefWarningShown) { - specialPropRefWarningShown = true; - warning( - false, - '%s: `ref` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://fb.me/react-special-props)', - typeof type === 'function' && 'displayName' in type ? type.displayName : 'Element' - ); - } - return undefined; - }, - configurable: true, - }); + if (key || ref) { + if ( + typeof props.$$typeof === 'undefined' || + props.$$typeof !== REACT_ELEMENT_TYPE + ) { + var displayName = typeof type === 'function' + ? type.displayName || type.name || 'Unknown' + : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } } @@ -221,3 +266,3 @@ ReactElement.createElement = function(type, config, children) { ReactCurrentOwner.current, - props + props, ); @@ -225,2 +270,6 @@ ReactElement.createElement = function(type, config, children) { +/** + * Return a function that produces ReactElements of a given type. + * See https://facebook.github.io/react/docs/top-level-api.html#react.createfactory + */ ReactElement.createFactory = function(type) { @@ -244,3 +293,3 @@ ReactElement.cloneAndReplaceKey = function(oldElement, newKey) { oldElement._owner, - oldElement.props + oldElement.props, ); @@ -250,2 +299,6 @@ ReactElement.cloneAndReplaceKey = function(oldElement, newKey) { +/** + * Clone and return a new ReactElement using element as the starting point. + * See https://facebook.github.io/react/docs/top-level-api.html#react.cloneelement + */ ReactElement.cloneElement = function(element, config, children) { @@ -270,3 +323,3 @@ ReactElement.cloneElement = function(element, config, children) { if (config != null) { - if (config.ref !== undefined) { + if (hasValidRef(config)) { // Silently steal the ref from the parent. @@ -275,5 +328,6 @@ ReactElement.cloneElement = function(element, config, children) { } - if (config.key !== undefined) { + if (hasValidKey(config)) { key = '' + config.key; } + // Remaining properties override existing props @@ -284,4 +338,6 @@ ReactElement.cloneElement = function(element, config, children) { for (propName in config) { - if (config.hasOwnProperty(propName) && - !RESERVED_PROPS.hasOwnProperty(propName)) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { if (config[propName] === undefined && defaultProps !== undefined) { @@ -309,11 +365,3 @@ ReactElement.cloneElement = function(element, config, children) { - return ReactElement( - element.type, - key, - ref, - self, - source, - owner, - props - ); + return ReactElement(element.type, key, ref, self, source, owner, props); }; @@ -321,2 +369,4 @@ ReactElement.cloneElement = function(element, config, children) { /** + * Verifies the object is a ReactElement. + * See https://facebook.github.io/react/docs/top-level-api.html#react.isvalidelement * @param {?object} object diff --git a/src/isomorphic/classic/element/ReactElementType.js b/src/isomorphic/classic/element/ReactElementType.js new file mode 100644 index 000000000..a0d8427b1 --- /dev/null +++ b/src/isomorphic/classic/element/ReactElementType.js @@ -0,0 +1,37 @@ +/** + * Copyright 2016-present, 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. + * + * @flow + * @providesModule ReactElementType + */ + +'use strict'; + +import type {ReactInstance} from 'ReactInstanceType'; + +export type Source = { + fileName: string, + lineNumber: number, +}; + +export type ReactElement = { + $$typeof: any, + type: any, + key: any, + ref: any, + props: any, + _owner: ReactInstance, + + // __DEV__ + _store: { + validated: boolean, + }, + _self: ReactElement, + _shadowChildren: any, + _source: Source, +}; diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 2051a212e..17f0332f8 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -20,6 +20,7 @@ -var ReactElement = require('ReactElement'); -var ReactPropTypeLocations = require('ReactPropTypeLocations'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactComponentTreeHook = require('ReactComponentTreeHook'); +var ReactElement = require('ReactElement'); + +var checkReactTypeSpec = require('checkReactTypeSpec'); @@ -27,4 +28,4 @@ var canDefineProperty = require('canDefineProperty'); var getIteratorFn = require('getIteratorFn'); -var invariant = require('invariant'); var warning = require('warning'); +var lowPriorityWarning = require('lowPriorityWarning'); @@ -40,2 +41,16 @@ function getDeclarationErrorAddendum() { +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 ''; +} + /** @@ -47,3 +62,15 @@ var ownerHasKeyUseWarning = {}; -var loggedTypeFailures = {}; +function getCurrentComponentErrorInfo(parentType) { + var info = getDeclarationErrorAddendum(); + + if (!info) { + var parentName = typeof parentType === 'string' + ? parentType + : parentType.displayName || parentType.name; + if (parentName) { + info = ` Check the top-level render call using <${parentName}>.`; + } + } + return info; +} @@ -53,3 +80,4 @@ var loggedTypeFailures = {}; * reordered. All children that haven't already been validated are required to - * have a "key" property assigned to it. + * have a "key" property assigned to it. Error statuses are cached so a warning + * will only be shown once. * @@ -65,50 +93,10 @@ function validateExplicitKey(element, parentType) { - var addenda = getAddendaForKeyUse('uniqueKey', element, parentType); - if (addenda === null) { - // we already showed the warning - return; - } - warning( - false, - 'Each child in an array or iterator should have a unique "key" prop.' + - '%s%s%s', - addenda.parentOrOwner || '', - addenda.childOwner || '', - addenda.url || '' - ); -} - -/** - * Shared warning and monitoring code for the key warnings. - * - * @internal - * @param {string} messageType A key used for de-duping warnings. - * @param {ReactElement} element Component that requires a key. - * @param {*} parentType element's parent's type. - * @returns {?object} A set of addenda to use in the warning message, or null - * if the warning has already been shown before (and shouldn't be shown again). - */ -function getAddendaForKeyUse(messageType, element, parentType) { - var addendum = getDeclarationErrorAddendum(); - if (!addendum) { - var parentName = typeof parentType === 'string' ? - parentType : parentType.displayName || parentType.name; - if (parentName) { - addendum = ` Check the top-level render call using <${parentName}>.`; - } - } + var memoizer = + ownerHasKeyUseWarning.uniqueKey || (ownerHasKeyUseWarning.uniqueKey = {}); - var memoizer = ownerHasKeyUseWarning[messageType] || ( - ownerHasKeyUseWarning[messageType] = {} - ); - if (memoizer[addendum]) { - return null; + var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType); + if (memoizer[currentComponentErrorInfo]) { + return; } - memoizer[addendum] = true; - - var addenda = { - parentOrOwner: addendum, - url: ' See https://fb.me/react-warning-keys for more information.', - childOwner: null, - }; + memoizer[currentComponentErrorInfo] = true; @@ -117,11 +105,20 @@ function getAddendaForKeyUse(messageType, element, parentType) { // assigning it a key. - if (element && - element._owner && - element._owner !== ReactCurrentOwner.current) { + var childOwner = ''; + if ( + element && + element._owner && + element._owner !== ReactCurrentOwner.current + ) { // Give the component that originally created this child. - addenda.childOwner = - ` It was passed a child from ${element._owner.getName()}.`; + childOwner = ` It was passed a child from ${element._owner.getName()}.`; } - return addenda; + warning( + false, + 'Each child in an array or iterator should have a unique "key" prop.' + + '%s%s See https://fb.me/react-warning-keys for more information.%s', + currentComponentErrorInfo, + childOwner, + ReactComponentTreeHook.getCurrentStackAddendum(element), + ); } @@ -170,57 +167,2 @@ function validateChildKeys(node, parentType) { -/** - * Assert that the props are valid - * - * @param {string} componentName Name of the component for error messages. - * @param {object} propTypes Map of prop name to a ReactPropType - * @param {object} props - * @param {string} location e.g. "prop", "context", "child context" - * @private - */ -function checkPropTypes(componentName, propTypes, props, location) { - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error; - // Prop type validation may throw. In case they do, we don't want to - // fail the render phase where it didn't fail before. So we log it. - // After these have been cleaned up, we'll let them throw. - try { - // This is intentionally an invariant that gets caught. It's the same - // behavior as without this statement except with a better message. - invariant( - typeof propTypes[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually from ' + - 'React.PropTypes.', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName - ); - error = propTypes[propName](props, propName, componentName, location); - } catch (ex) { - error = ex; - } - warning( - !error || error instanceof Error, - '%s: type specification of %s `%s` is invalid; the type checker ' + - 'function must return `null` or an `Error` but returned a %s. ' + - 'You may have forgotten to pass an argument to the type checker ' + - 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + - 'shape all require an argument).', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName, - typeof error - ); - if (error instanceof Error && !(error.message in loggedTypeFailures)) { - // Only monitor this failure once because there tends to be a lot of the - // same error. - loggedTypeFailures[error.message] = true; - - var addendum = getDeclarationErrorAddendum(); - warning(false, 'Failed propType: %s%s', error.message, addendum); - } - } - } -} - /** @@ -238,7 +180,9 @@ function validatePropTypes(element) { if (componentClass.propTypes) { - checkPropTypes( - name, + checkReactTypeSpec( componentClass.propTypes, element.props, - ReactPropTypeLocations.prop + 'prop', + name, + element, + null, ); @@ -249,3 +193,3 @@ function validatePropTypes(element) { 'getDefaultProps is only used on classic React.createClass ' + - 'definitions. Use a static property named `defaultProps` instead.' + 'definitions. Use a static property named `defaultProps` instead.', ); @@ -255,3 +199,2 @@ function validatePropTypes(element) { var ReactElementValidator = { - createElement: function(type, props, children) { @@ -260,9 +203,42 @@ var ReactElementValidator = { // succeed and there will likely be errors in render. - warning( - validType, - 'React.createElement: type should not be null, undefined, boolean, or ' + - 'number. It should be a string (for DOM elements) or a ReactClass ' + - '(for composite components).%s', - getDeclarationErrorAddendum() - ); + if (!validType) { + if (typeof type !== 'function' && typeof type !== 'string') { + var info = ''; + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in."; + } + + var sourceInfo = getSourceInfoErrorAddendum(props); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + + info += ReactComponentTreeHook.getCurrentStackAddendum(); + + var currentSource = props !== null && + props !== undefined && + props.__source !== undefined + ? props.__source + : null; + ReactComponentTreeHook.pushNonStandardWarningStack(true, currentSource); + warning( + false, + 'React.createElement: type is invalid -- expected a string (for ' + + 'built-in components) or a class/function (for composite ' + + 'components) but got: %s.%s', + type == null ? type : typeof type, + info, + ); + ReactComponentTreeHook.popNonStandardWarningStack(); + } + } @@ -293,6 +269,3 @@ var ReactElementValidator = { createFactory: function(type) { - var validatedFactory = ReactElementValidator.createElement.bind( - null, - type - ); + var validatedFactory = ReactElementValidator.createElement.bind(null, type); // Legacy hook TODO: Warn if this is accessed @@ -302,20 +275,16 @@ var ReactElementValidator = { if (canDefineProperty) { - Object.defineProperty( - validatedFactory, - 'type', - { - enumerable: false, - get: function() { - warning( - false, - 'Factory.type is deprecated. Access the class directly ' + - 'before passing it to createFactory.' - ); - Object.defineProperty(this, 'type', { - value: type, - }); - return type; - }, - } - ); + Object.defineProperty(validatedFactory, 'type', { + enumerable: false, + get: function() { + lowPriorityWarning( + false, + 'Factory.type is deprecated. Access the class directly ' + + 'before passing it to createFactory.', + ); + Object.defineProperty(this, 'type', { + value: type, + }); + return type; + }, + }); } @@ -323,3 +292,2 @@ var ReactElementValidator = { - return validatedFactory; @@ -335,3 +303,2 @@ var ReactElementValidator = { }, - }; diff --git a/src/isomorphic/classic/element/__tests__/ReactElement-test.js b/src/isomorphic/classic/element/__tests__/ReactElement-test.js index 402c9b95e..de83b4e91 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElement-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElement-test.js @@ -17,3 +17,3 @@ var ReactTestUtils; -describe('ReactElement', function() { +describe('ReactElement', () => { var ComponentClass; @@ -21,3 +21,3 @@ describe('ReactElement', function() { - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -34,10 +34,10 @@ describe('ReactElement', function() { // classic JS without JSX. - ComponentClass = React.createClass({ - render: function() { + ComponentClass = class extends React.Component { + render() { return React.createElement('div'); - }, - }); + } + }; }); - afterEach(function() { + afterEach(() => { global.Symbol = originalSymbol; @@ -45,3 +45,3 @@ describe('ReactElement', function() { - it('uses the fallback value when in an environment without Symbol', function() { + it('uses the fallback value when in an environment without Symbol', () => { expect(
.$$typeof).toBe(0xeac7); @@ -49,3 +49,3 @@ describe('ReactElement', function() { - it('returns a complete element according to spec', function() { + it('returns a complete element according to spec', () => { var element = React.createFactory(ComponentClass)(); @@ -54,17 +54,17 @@ describe('ReactElement', function() { expect(element.ref).toBe(null); - var expectation = {}; - Object.freeze(expectation); - expect(element.props).toEqual(expectation); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({}); }); - it('should warn when `key` is being accessed', function() { + it('should warn when `key` is being accessed on composite element', () => { spyOn(console, 'error'); var container = document.createElement('div'); - var Child = React.createClass({ - render: function() { + class Child extends React.Component { + render() { return
{this.props.key}
; - }, - }); - var Parent = React.createClass({ - render: function() { + } + } + class Parent extends React.Component { + render() { return ( @@ -76,12 +76,26 @@ describe('ReactElement', function() { ); - }, - }); - expect(console.error.calls.length).toBe(0); + } + } + expect(console.error.calls.count()).toBe(0); ReactDOM.render(, container); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'Child: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://fb.me/react-special-props)' + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + ); + }); + + it('should warn when `key` is being accessed on a host element', () => { + spyOn(console, 'error'); + var element =
; + expect(console.error.calls.count()).toBe(0); + void element.props.key; + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', ); @@ -89,12 +103,12 @@ describe('ReactElement', function() { - it('should warn when `ref` is being accessed', function() { + it('should warn when `ref` is being accessed', () => { spyOn(console, 'error'); var container = document.createElement('div'); - var Child = React.createClass({ - render: function() { + class Child extends React.Component { + render() { return
{this.props.ref}
; - }, - }); - var Parent = React.createClass({ - render: function() { + } + } + class Parent extends React.Component { + render() { return ( @@ -104,12 +118,12 @@ describe('ReactElement', function() { ); - }, - }); - expect(console.error.calls.length).toBe(0); + } + } + expect(console.error.calls.count()).toBe(0); ReactDOM.render(, container); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'Child: `ref` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://fb.me/react-special-props)' + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', ); @@ -117,3 +131,3 @@ describe('ReactElement', function() { - it('allows a string to be passed as the type', function() { + it('allows a string to be passed as the type', () => { var element = React.createFactory('div')(); @@ -122,13 +136,13 @@ describe('ReactElement', function() { expect(element.ref).toBe(null); - var expectation = {}; - Object.freeze(expectation); - expect(element.props).toEqual(expectation); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({}); }); - it('returns an immutable element', function() { + it('returns an immutable element', () => { var element = React.createFactory(ComponentClass)(); - expect(() => element.type = 'div').toThrow(); + expect(() => (element.type = 'div')).toThrow(); }); - it('does not reuse the original config object', function() { + it('does not reuse the original config object', () => { var config = {foo: 1}; @@ -140,3 +154,9 @@ describe('ReactElement', function() { - it('extracts key and ref from the config', function() { + it('does not fail if config has no prototype', () => { + var config = Object.create(null, {foo: {value: 1, enumerable: true}}); + var element = React.createFactory(ComponentClass)(config); + expect(element.props.foo).toBe(1); + }); + + it('extracts key and ref from the config', () => { var element = React.createFactory(ComponentClass)({ @@ -149,8 +169,44 @@ describe('ReactElement', function() { expect(element.ref).toBe('34'); - var expectation = {foo:'56'}; - Object.freeze(expectation); - expect(element.props).toEqual(expectation); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({foo: '56'}); }); - it('coerces the key to a string', function() { + it('extracts null key and ref', () => { + var element = React.createFactory(ComponentClass)({ + key: null, + ref: null, + foo: '12', + }); + expect(element.type).toBe(ComponentClass); + expect(element.key).toBe('null'); + expect(element.ref).toBe(null); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({foo: '12'}); + }); + + it('ignores undefined key and ref', () => { + var props = { + foo: '56', + key: undefined, + ref: undefined, + }; + var element = React.createFactory(ComponentClass)(props); + expect(element.type).toBe(ComponentClass); + expect(element.key).toBe(null); + expect(element.ref).toBe(null); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({foo: '56'}); + }); + + it('ignores key and ref warning getters', () => { + var elementA = React.createElement('div'); + var elementB = React.createElement('div', elementA.props); + expect(elementB.key).toBe(null); + expect(elementB.ref).toBe(null); + }); + + it('coerces the key to a string', () => { var element = React.createFactory(ComponentClass)({ @@ -162,8 +218,8 @@ describe('ReactElement', function() { expect(element.ref).toBe(null); - var expectation = {foo:'56'}; - Object.freeze(expectation); - expect(element.props).toEqual(expectation); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(element.props).toEqual({foo: '56'}); }); - it('preserves the owner on the element', function() { + it('preserves the owner on the element', () => { var Component = React.createFactory(ComponentClass); @@ -171,11 +227,11 @@ describe('ReactElement', function() { - var Wrapper = React.createClass({ - render: function() { + class Wrapper extends React.Component { + render() { element = Component(); return element; - }, - }); + } + } var instance = ReactTestUtils.renderIntoDocument( - React.createElement(Wrapper) + React.createElement(Wrapper), ); @@ -185,13 +241,16 @@ describe('ReactElement', function() { - it('merges an additional argument onto the children prop', function() { + it('merges an additional argument onto the children prop', () => { spyOn(console, 'error'); var a = 1; - var element = React.createFactory(ComponentClass)({ - children: 'text', - }, a); + var element = React.createFactory(ComponentClass)( + { + children: 'text', + }, + a, + ); expect(element.props.children).toBe(a); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not override children if no rest args are provided', function() { + it('does not override children if no rest args are provided', () => { spyOn(console, 'error'); @@ -201,15 +260,18 @@ describe('ReactElement', function() { expect(element.props.children).toBe('text'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('overrides children if null is provided as an argument', function() { + it('overrides children if null is provided as an argument', () => { spyOn(console, 'error'); - var element = React.createFactory(ComponentClass)({ - children: 'text', - }, null); + var element = React.createFactory(ComponentClass)( + { + children: 'text', + }, + null, + ); expect(element.props.children).toBe(null); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('merges rest arguments onto the children prop in an array', function() { + it('merges rest arguments onto the children prop in an array', () => { spyOn(console, 'error'); @@ -220,3 +282,3 @@ describe('ReactElement', function() { expect(element.props.children).toEqual([1, 2, 3]); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -225,18 +287,11 @@ describe('ReactElement', function() { // classic JS without JSX. - it('allows static methods to be called using the type property', function() { + it('allows static methods to be called using the type property', () => { spyOn(console, 'error'); - var StaticMethodComponentClass = React.createClass({ - statics: { - someStaticMethod: function() { - return 'someReturnValue'; - }, - }, - getInitialState: function() { - return {valueToReturn: 'hi'}; - }, - render: function() { + class StaticMethodComponentClass extends React.Component { + render() { return React.createElement('div'); - }, - }); + } + } + StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue'; @@ -244,3 +299,3 @@ describe('ReactElement', function() { expect(element.type.someStaticMethod()).toBe('someReturnValue'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -249,13 +304,11 @@ describe('ReactElement', function() { // classic JS without JSX. - it('identifies valid elements', function() { - var Component = React.createClass({ - render: function() { + it('identifies valid elements', () => { + class Component extends React.Component { + render() { return React.createElement('div'); - }, - }); + } + } - expect(React.isValidElement(React.createElement('div'))) - .toEqual(true); - expect(React.isValidElement(React.createElement(Component))) - .toEqual(true); + expect(React.isValidElement(React.createElement('div'))).toEqual(true); + expect(React.isValidElement(React.createElement(Component))).toEqual(true); @@ -265,5 +318,5 @@ describe('ReactElement', function() { expect(React.isValidElement('string')).toEqual(false); - expect(React.isValidElement(React.DOM.div)).toEqual(false); + expect(React.isValidElement(React.createFactory('div'))).toEqual(false); expect(React.isValidElement(Component)).toEqual(false); - expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false); + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); @@ -273,20 +326,5 @@ describe('ReactElement', function() { - 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 precaution. - var Component = React.createClass({ - render: () => null, - statics: { - specialType: React.PropTypes.shape({monkey: React.PropTypes.any}), - }, - }); - - expect(typeof Component.specialType).toBe('function'); - expect(typeof Component.specialType.isRequired).toBe('function'); - }); - // NOTE: We're explicitly not using JSX here. This is intended to test // classic JS without JSX. - it('is indistinguishable from a plain object', function() { + it('is indistinguishable from a plain object', () => { var element = React.createElement('div', {className: 'foo'}); @@ -298,11 +336,9 @@ describe('ReactElement', function() { // classic JS without JSX. - it('should use default prop value when removing a prop', function() { - var Component = React.createClass({ - getDefaultProps: function() { - return {fruit: 'persimmon'}; - }, - render: function() { + it('should use default prop value when removing a prop', () => { + class Component extends React.Component { + render() { return React.createElement('span'); - }, - }); + } + } + Component.defaultProps = {fruit: 'persimmon'}; @@ -311,3 +347,3 @@ describe('ReactElement', function() { React.createElement(Component, {fruit: 'mango'}), - container + container, ); @@ -321,14 +357,12 @@ describe('ReactElement', function() { // classic JS without JSX. - it('should normalize props with default values', function() { - var Component = React.createClass({ - getDefaultProps: function() { - return {prop: 'testKey'}; - }, - render: function() { + it('should normalize props with default values', () => { + class Component extends React.Component { + render() { return React.createElement('span', null, this.props.prop); - }, - }); + } + } + Component.defaultProps = {prop: 'testKey'}; var instance = ReactTestUtils.renderIntoDocument( - React.createElement(Component) + React.createElement(Component), ); @@ -337,3 +371,3 @@ describe('ReactElement', function() { var inst2 = ReactTestUtils.renderIntoDocument( - React.createElement(Component, {prop: null}) + React.createElement(Component, {prop: null}), ); @@ -342,28 +376,5 @@ describe('ReactElement', function() { - it('should normalize props with default values in cloning', function() { - var Component = React.createClass({ - getDefaultProps: function() { - return {prop: 'testKey'}; - }, - render: function() { - return ; - }, - }); - - var instance = React.createElement(Component); - var clonedInstance = React.cloneElement(instance, {prop: undefined}); - expect(clonedInstance.props.prop).toBe('testKey'); - var clonedInstance2 = React.cloneElement(instance, {prop: null}); - expect(clonedInstance2.props.prop).toBe(null); - - var instance2 = React.createElement(Component, {prop: 'newTestKey'}); - var cloneInstance3 = React.cloneElement(instance2, {prop: undefined}); - expect(cloneInstance3.props.prop).toBe('testKey'); - var cloneInstance4 = React.cloneElement(instance2, {}); - expect(cloneInstance4.props.prop).toBe('newTestKey'); - }); - - it('throws when changing a prop (in dev) after element creation', function() { - var Outer = React.createClass({ - render: function() { + it('throws when changing a prop (in dev) after element creation', () => { + class Outer extends React.Component { + render() { var el =
; @@ -376,4 +387,4 @@ describe('ReactElement', function() { return el; - }, - }); + } + } var outer = ReactTestUtils.renderIntoDocument(); @@ -382,7 +393,6 @@ describe('ReactElement', function() { - it('throws when adding a prop (in dev) after element creation', function() { + it('throws when adding a prop (in dev) after element creation', () => { var container = document.createElement('div'); - var Outer = React.createClass({ - getDefaultProps: () => ({sound: 'meow'}), - render: function() { + class Outer extends React.Component { + render() { var el =
{this.props.sound}
; @@ -396,4 +406,5 @@ describe('ReactElement', function() { return el; - }, - }); + } + } + Outer.defaultProps = {sound: 'meow'}; var outer = ReactDOM.render(, container); @@ -403,12 +414,12 @@ describe('ReactElement', function() { - it('does not warn for NaN props', function() { + it('does not warn for NaN props', () => { spyOn(console, 'error'); - var Test = React.createClass({ - render: function() { + class Test extends React.Component { + render() { return
; - }, - }); + } + } var test = ReactTestUtils.renderIntoDocument(); expect(test.props.value).toBeNaN(); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -417,3 +428,3 @@ describe('ReactElement', function() { // classic JS without JSX. - it('identifies elements, but not JSON, if Symbols are supported', function() { + it('identifies elements, but not JSON, if Symbols are supported', () => { // Rudimentary polyfill @@ -437,12 +448,10 @@ describe('ReactElement', function() { - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { return React.createElement('div'); - }, - }); + } + } - expect(React.isValidElement(React.createElement('div'))) - .toEqual(true); - expect(React.isValidElement(React.createElement(Component))) - .toEqual(true); + expect(React.isValidElement(React.createElement('div'))).toEqual(true); + expect(React.isValidElement(React.createElement(Component))).toEqual(true); @@ -452,5 +461,5 @@ describe('ReactElement', function() { expect(React.isValidElement('string')).toEqual(false); - expect(React.isValidElement(React.DOM.div)).toEqual(false); + expect(React.isValidElement(React.createFactory('div'))).toEqual(false); expect(React.isValidElement(Component)).toEqual(false); - expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false); + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); @@ -459,9 +468,8 @@ describe('ReactElement', function() { }); - }); -describe('comparing jsx vs .createFactory() vs .createElement()', function() { +describe('comparing jsx vs .createFactory() vs .createElement()', () => { var Child; - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -473,8 +481,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - - describe('when using jsx only', function() { + describe('when using jsx only', () => { var Parent, instance; - beforeEach(function() { - Parent = React.createClass({ - render: function() { + beforeEach(() => { + Parent = class extends React.Component { + render() { return ( @@ -484,9 +491,12 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { ); - }, - }); - instance = ReactTestUtils.renderIntoDocument(); + } + }; + instance = ReactTestUtils.renderIntoDocument(); }); - it('should scry children but cannot', function() { - var children = ReactTestUtils.scryRenderedComponentsWithType(instance, Child); + it('should scry children but cannot', () => { + var children = ReactTestUtils.scryRenderedComponentsWithType( + instance, + Child, + ); expect(children.length).toBe(1); @@ -494,3 +504,3 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('does not maintain refs', function() { + it('does not maintain refs', () => { expect(instance.refs.child).not.toBeUndefined(); @@ -498,4 +508,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('can capture Child instantiation calls', function() { - expect(Child.mock.calls[0][0]).toEqual({ foo: 'foo value', children: 'children value' }); + it('can capture Child instantiation calls', () => { + expect(Child.mock.calls[0][0]).toEqual({ + foo: 'foo value', + children: 'children value', + }); }); @@ -503,11 +516,15 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - describe('when using parent that uses .createFactory()', function() { + describe('when using parent that uses .createFactory()', () => { var factory, instance; - beforeEach(function() { + beforeEach(() => { var childFactory = React.createFactory(Child); - var Parent = React.createClass({ - render: function() { - return React.DOM.div({}, childFactory({ ref: 'child', foo: 'foo value' }, 'children value')); - }, - }); + class Parent extends React.Component { + render() { + return React.createElement( + 'div', + {}, + childFactory({ref: 'child', foo: 'foo value'}, 'children value'), + ); + } + } factory = React.createFactory(Parent); @@ -516,4 +533,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('can properly scry children', function() { - var children = ReactTestUtils.scryRenderedComponentsWithType(instance, Child); + it('can properly scry children', () => { + var children = ReactTestUtils.scryRenderedComponentsWithType( + instance, + Child, + ); expect(children.length).toBe(1); @@ -521,3 +541,3 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('does not maintain refs', function() { + it('does not maintain refs', () => { expect(instance.refs.child).not.toBeUndefined(); @@ -525,4 +545,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('can capture Child instantiation calls', function() { - expect(Child.mock.calls[0][0]).toEqual({ foo: 'foo value', children: 'children value' }); + it('can capture Child instantiation calls', () => { + expect(Child.mock.calls[0][0]).toEqual({ + foo: 'foo value', + children: 'children value', + }); }); @@ -530,10 +553,18 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - describe('when using parent that uses .createElement()', function() { + describe('when using parent that uses .createElement()', () => { var factory, instance; - beforeEach(function() { - var Parent = React.createClass({ - render: function() { - return React.DOM.div({}, React.createElement(Child, { ref: 'child', foo: 'foo value' }, 'children value')); - }, - }); + beforeEach(() => { + class Parent extends React.Component { + render() { + return React.createElement( + 'div', + {}, + React.createElement( + Child, + {ref: 'child', foo: 'foo value'}, + 'children value', + ), + ); + } + } factory = React.createFactory(Parent); @@ -542,4 +573,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('should scry children but cannot', function() { - var children = ReactTestUtils.scryRenderedComponentsWithType(instance, Child); + it('should scry children but cannot', () => { + var children = ReactTestUtils.scryRenderedComponentsWithType( + instance, + Child, + ); expect(children.length).toBe(1); @@ -547,3 +581,3 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('does not maintain refs', function() { + it('does not maintain refs', () => { expect(instance.refs.child).not.toBeUndefined(); @@ -551,4 +585,7 @@ describe('comparing jsx vs .createFactory() vs .createElement()', function() { - it('can capture Child instantiation calls', function() { - expect(Child.mock.calls[0][0]).toEqual({ foo: 'foo value', children: 'children value' }); + it('can capture Child instantiation calls', () => { + expect(Child.mock.calls[0][0]).toEqual({ + foo: 'foo value', + children: 'children value', + }); }); diff --git a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js index f4c5b4b76..a29bdff98 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js @@ -13,2 +13,3 @@ +var PropTypes; var React; @@ -17,5 +18,6 @@ var ReactTestUtils; -describe('ReactElementClone', function() { +describe('ReactElementClone', () => { + var ComponentClass; - beforeEach(function() { + beforeEach(() => { React = require('React'); @@ -23,19 +25,28 @@ describe('ReactElementClone', function() { ReactTestUtils = require('ReactTestUtils'); + PropTypes = require('prop-types'); + + // NOTE: We're explicitly not using JSX here. This is intended to test + // classic JS without JSX. + ComponentClass = class extends React.Component { + render() { + return React.createElement('div'); + } + }; }); - it('should clone a DOM component with new props', function() { - var Grandparent = React.createClass({ - render: function() { + it('should clone a DOM component with new props', () => { + class Grandparent extends React.Component { + render() { return } />; - }, - }); - var Parent = React.createClass({ - render: function() { + } + } + class Parent extends React.Component { + render() { return (
- {React.cloneElement(this.props.child, { className: 'xyz' })} + {React.cloneElement(this.props.child, {className: 'xyz'})}
); - }, - }); + } + } var component = ReactTestUtils.renderIntoDocument(); @@ -44,22 +55,22 @@ describe('ReactElementClone', function() { - it('should clone a composite component with new props', function() { - var Child = React.createClass({ - render: function() { + it('should clone a composite component with new props', () => { + class Child extends React.Component { + render() { return
; - }, - }); - var Grandparent = React.createClass({ - render: function() { + } + } + class Grandparent extends React.Component { + render() { return } />; - }, - }); - var Parent = React.createClass({ - render: function() { + } + } + class Parent extends React.Component { + render() { return (
- {React.cloneElement(this.props.child, { className: 'xyz' })} + {React.cloneElement(this.props.child, {className: 'xyz'})}
); - }, - }); + } + } var component = ReactTestUtils.renderIntoDocument(); @@ -68,18 +79,23 @@ describe('ReactElementClone', function() { - it('should keep the original ref if it is not overridden', function() { - var Grandparent = React.createClass({ - render: function() { + it('does not fail if config has no prototype', () => { + var config = Object.create(null, {foo: {value: 1, enumerable: true}}); + React.cloneElement(
, config); + }); + + it('should keep the original ref if it is not overridden', () => { + class Grandparent extends React.Component { + render() { return } />; - }, - }); + } + } - var Parent = React.createClass({ - render: function() { + class Parent extends React.Component { + render() { return (
- {React.cloneElement(this.props.child, { className: 'xyz' })} + {React.cloneElement(this.props.child, {className: 'xyz'})}
); - }, - }); + } + } @@ -89,8 +105,8 @@ describe('ReactElementClone', function() { - it('should transfer the key property', function() { - var Component = React.createClass({ - render: function() { + it('should transfer the key property', () => { + class Component extends React.Component { + render() { return null; - }, - }); + } + } var clone = React.cloneElement(, {key: 'xyz'}); @@ -99,12 +115,12 @@ describe('ReactElementClone', function() { - it('should transfer children', function() { - var Component = React.createClass({ - render: function() { + it('should transfer children', () => { + class Component extends React.Component { + render() { expect(this.props.children).toBe('xyz'); return
; - }, - }); + } + } ReactTestUtils.renderIntoDocument( - React.cloneElement(, {children: 'xyz'}) + React.cloneElement(, {children: 'xyz'}), ); @@ -112,12 +128,12 @@ describe('ReactElementClone', function() { - it('should shallow clone children', function() { - var Component = React.createClass({ - render: function() { + it('should shallow clone children', () => { + class Component extends React.Component { + render() { expect(this.props.children).toBe('xyz'); return
; - }, - }); + } + } ReactTestUtils.renderIntoDocument( - React.cloneElement(xyz, {}) + React.cloneElement(xyz, {}), ); @@ -125,8 +141,8 @@ describe('ReactElementClone', function() { - it('should accept children as rest arguments', function() { - var Component = React.createClass({ - render: function() { + it('should accept children as rest arguments', () => { + class Component extends React.Component { + render() { return null; - }, - }); + } + } @@ -134,18 +150,37 @@ describe('ReactElementClone', function() { xyz, - { children: }, + {children: },
, - + , ); - expect(clone.props.children).toEqual([ -
, - , - ]); + expect(clone.props.children).toEqual([
, ]); }); - it('should support keys and refs', function() { - var Parent = React.createClass({ - render: function() { - var clone = - React.cloneElement(this.props.children, {key: 'xyz', ref: 'xyz'}); + it('should override children if undefined is provided as an argument', () => { + var element = React.createElement( + ComponentClass, + { + children: 'text', + }, + undefined, + ); + expect(element.props.children).toBe(undefined); + + var element2 = React.cloneElement( + React.createElement(ComponentClass, { + children: 'text', + }), + {}, + undefined, + ); + expect(element2.props.children).toBe(undefined); + }); + + it('should support keys and refs', () => { + class Parent extends React.Component { + render() { + var clone = React.cloneElement(this.props.children, { + key: 'xyz', + ref: 'xyz', + }); expect(clone.key).toBe('xyz'); @@ -153,10 +188,10 @@ describe('ReactElementClone', function() { return
{clone}
; - }, - }); + } + } - var Grandparent = React.createClass({ - render: function() { + class Grandparent extends React.Component { + render() { return ; - }, - }); + } + } @@ -166,15 +201,15 @@ describe('ReactElementClone', function() { - it('should steal the ref if a new ref is specified', function() { - var Parent = React.createClass({ - render: function() { + it('should steal the ref if a new ref is specified', () => { + class Parent extends React.Component { + render() { var clone = React.cloneElement(this.props.children, {ref: 'xyz'}); return
{clone}
; - }, - }); + } + } - var Grandparent = React.createClass({ - render: function() { + class Grandparent extends React.Component { + render() { return ; - }, - }); + } + } @@ -185,12 +220,12 @@ describe('ReactElementClone', function() { - it('should overwrite props', function() { - var Component = React.createClass({ - render: function() { + it('should overwrite props', () => { + class Component extends React.Component { + render() { expect(this.props.myprop).toBe('xyz'); return
; - }, - }); + } + } ReactTestUtils.renderIntoDocument( - React.cloneElement(, {myprop: 'xyz'}) + React.cloneElement(, {myprop: 'xyz'}), ); @@ -198,3 +233,24 @@ describe('ReactElementClone', function() { - it('warns for keys for arrays of elements in rest args', function() { + it('should normalize props with default values', () => { + class Component extends React.Component { + render() { + return ; + } + } + Component.defaultProps = {prop: 'testKey'}; + + var instance = React.createElement(Component); + var clonedInstance = React.cloneElement(instance, {prop: undefined}); + expect(clonedInstance.props.prop).toBe('testKey'); + var clonedInstance2 = React.cloneElement(instance, {prop: null}); + expect(clonedInstance2.props.prop).toBe(null); + + var instance2 = React.createElement(Component, {prop: 'newTestKey'}); + var cloneInstance3 = React.cloneElement(instance2, {prop: undefined}); + expect(cloneInstance3.props.prop).toBe('testKey'); + var cloneInstance4 = React.cloneElement(instance2, {}); + expect(cloneInstance4.props.prop).toBe('newTestKey'); + }); + + it('warns for keys for arrays of elements in rest args', () => { spyOn(console, 'error'); @@ -203,5 +259,5 @@ describe('ReactElementClone', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.', ); @@ -209,3 +265,3 @@ describe('ReactElementClone', function() { - it('does not warns for arrays of elements with keys', function() { + it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); @@ -214,6 +270,6 @@ describe('ReactElementClone', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the element is directly in rest args', function() { + it('does not warn when the element is directly in rest args', () => { spyOn(console, 'error'); @@ -222,6 +278,6 @@ describe('ReactElementClone', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the array contains a non-element', function() { + it('does not warn when the array contains a non-element', () => { spyOn(console, 'error'); @@ -230,34 +286,36 @@ describe('ReactElementClone', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('should check declared prop types after clone', function() { + it('should check declared prop types after clone', () => { spyOn(console, 'error'); - var Component = React.createClass({ - propTypes: { - color: React.PropTypes.string.isRequired, - }, - render: function() { + class Component extends React.Component { + render() { return React.createElement('div', null, 'My color is ' + this.color); - }, - }); - var Parent = React.createClass({ - render: function() { + } + } + Component.propTypes = { + color: PropTypes.string.isRequired, + }; + class Parent extends React.Component { + render() { return React.cloneElement(this.props.child, {color: 123}); - }, - }); - var GrandParent = React.createClass({ - render: function() { - return React.createElement( - Parent, - { child: React.createElement(Component, {color: 'red'}) } - ); - }, - }); + } + } + class GrandParent extends React.Component { + render() { + return React.createElement(Parent, { + child: React.createElement(Component, {color: 'red'}), + }); + } + } ReactTestUtils.renderIntoDocument(React.createElement(GrandParent)); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Invalid prop `color` of type `number` supplied to `Component`, ' + - 'expected `string`. Check the render method of `Parent`.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Failed prop type: ' + + 'Invalid prop `color` of type `number` supplied to `Component`, ' + + 'expected `string`.\n' + + ' in Component (created by GrandParent)\n' + + ' in Parent (created by GrandParent)\n' + + ' in GrandParent', ); @@ -265,2 +323,48 @@ describe('ReactElementClone', function() { + it('should ignore key and ref warning getters', () => { + var elementA = React.createElement('div'); + var elementB = React.cloneElement(elementA, elementA.props); + expect(elementB.key).toBe(null); + expect(elementB.ref).toBe(null); + }); + + it('should ignore undefined key and ref', () => { + var element = React.createFactory(ComponentClass)({ + key: '12', + ref: '34', + foo: '56', + }); + var props = { + key: undefined, + ref: undefined, + foo: 'ef', + }; + var clone = React.cloneElement(element, props); + expect(clone.type).toBe(ComponentClass); + expect(clone.key).toBe('12'); + expect(clone.ref).toBe('34'); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(clone.props).toEqual({foo: 'ef'}); + }); + + it('should extract null key and ref', () => { + var element = React.createFactory(ComponentClass)({ + key: '12', + ref: '34', + foo: '56', + }); + var props = { + key: null, + ref: null, + foo: 'ef', + }; + var clone = React.cloneElement(element, props); + expect(clone.type).toBe(ComponentClass); + expect(clone.key).toBe('null'); + expect(clone.ref).toBe(null); + expect(Object.isFrozen(element)).toBe(true); + expect(Object.isFrozen(element.props)).toBe(true); + expect(clone.props).toEqual({foo: 'ef'}); + }); }); diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 3cdcd48e5..a772395aa 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -16,2 +16,3 @@ +var PropTypes; var React; @@ -20,6 +21,10 @@ var ReactTestUtils; -describe('ReactElementValidator', function() { +describe('ReactElementValidator', () => { + function normalizeCodeLocInfo(str) { + return str && str.replace(/at .+?:\d+/g, 'at **'); + } + var ComponentClass; - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -29,10 +34,11 @@ describe('ReactElementValidator', function() { ReactTestUtils = require('ReactTestUtils'); - ComponentClass = React.createClass({ - render: function() { + PropTypes = require('prop-types'); + ComponentClass = class extends React.Component { + render() { return React.createElement('div'); - }, - }); + } + }; }); - it('warns for keys for arrays of elements in rest args', function() { + it('warns for keys for arrays of elements in rest args', () => { spyOn(console, 'error'); @@ -42,5 +48,5 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.', ); @@ -48,3 +54,3 @@ describe('ReactElementValidator', function() { - it('warns for keys for arrays of elements with owner info', function() { + it('warns for keys for arrays of elements with owner info', () => { spyOn(console, 'error'); @@ -52,8 +58,7 @@ describe('ReactElementValidator', function() { - var InnerClass = React.createClass({ - displayName: 'InnerClass', - render: function() { + class InnerClass extends React.Component { + render() { return Component(null, this.props.childSet); - }, - }); + } + } @@ -61,18 +66,15 @@ describe('ReactElementValidator', function() { - var ComponentWrapper = React.createClass({ - displayName: 'ComponentWrapper', - render: function() { - return InnerComponent({childSet: [Component(), Component()] }); - }, - }); + class ComponentWrapper extends React.Component { + render() { + return InnerComponent({childSet: [Component(), Component()]}); + } + } - ReactTestUtils.renderIntoDocument( - React.createElement(ComponentWrapper) - ); + ReactTestUtils.renderIntoDocument(React.createElement(ComponentWrapper)); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(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. ' + 'Check the render method of `InnerClass`. ' + + 'It was passed a child from ComponentWrapper. ', ); @@ -80,22 +82,18 @@ describe('ReactElementValidator', function() { - it('warns for keys for arrays with no owner or parent info', function() { + it('warns for keys for arrays with no owner or parent info', () => { spyOn(console, 'error'); - var Anonymous = React.createClass({ - displayName: undefined, - render: function() { - return
; - }, - }); + function Anonymous() { + return
; + } + Object.defineProperty(Anonymous, 'name', {value: undefined}); - var divs = [ -
, -
, - ]; + var divs = [
,
]; ReactTestUtils.renderIntoDocument({divs}); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Each child in an array or iterator should have a unique ' + - '"key" prop. See https://fb.me/react-warning-keys for more information.' + '"key" prop. See https://fb.me/react-warning-keys for more information.\n' + + ' in div (at **)', ); @@ -103,16 +101,14 @@ describe('ReactElementValidator', function() { - it('warns for keys for arrays of elements with no owner info', function() { + it('warns for keys for arrays of elements with no owner info', () => { spyOn(console, 'error'); - var divs = [ -
, -
, - ]; + var divs = [
,
]; ReactTestUtils.renderIntoDocument(
{divs}
); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( 'Warning: Each child in an array or iterator should have a unique ' + - '"key" prop. Check the top-level render call using
. See ' + - 'https://fb.me/react-warning-keys for more information.' + '"key" prop. Check the top-level render call using
. See ' + + 'https://fb.me/react-warning-keys for more information.\n' + + ' in div (at **)', ); @@ -120,15 +116,42 @@ describe('ReactElementValidator', function() { - it('does not warn for keys when passing children down', function() { + it('warns for keys with component stack info', () => { spyOn(console, 'error'); - var Wrapper = React.createClass({ - render: function() { - return ( -
- {this.props.children} -
-
- ); - }, - }); + function Component() { + return
{[
,
]}
; + } + + function Parent(props) { + return React.cloneElement(props.child); + } + + function GrandParent() { + return } />; + } + + ReactTestUtils.renderIntoDocument(); + + expect(console.error.calls.count()).toBe(1); + expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( + 'Warning: Each child in an array or iterator should have a unique ' + + '"key" prop. Check the render method of `Component`. See ' + + 'https://fb.me/react-warning-keys for more information.\n' + + ' in div (at **)\n' + + ' in Component (at **)\n' + + ' in Parent (at **)\n' + + ' in GrandParent (at **)', + ); + }); + + it('does not warn for keys when passing children down', () => { + spyOn(console, 'error'); + + function Wrapper(props) { + return ( +
+ {props.children} +
+
+ ); + } @@ -138,9 +161,9 @@ describe('ReactElementValidator', function() { - + , ); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('warns for keys for iterables of elements in rest args', function() { + it('warns for keys for iterables of elements in rest args', () => { spyOn(console, 'error'); @@ -162,5 +185,5 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.', ); @@ -168,3 +191,3 @@ describe('ReactElementValidator', function() { - it('does not warns for arrays of elements with keys', function() { + it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); @@ -174,6 +197,6 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warns for iterable elements with keys', function() { + it('does not warns for iterable elements with keys', () => { spyOn(console, 'error'); @@ -198,6 +221,6 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the element is directly in rest args', function() { + it('does not warn when the element is directly in rest args', () => { spyOn(console, 'error'); @@ -207,6 +230,6 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the array contains a non-element', function() { + it('does not warn when the array contains a non-element', () => { spyOn(console, 'error'); @@ -216,3 +239,3 @@ describe('ReactElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -227,20 +250,18 @@ describe('ReactElementValidator', function() { spyOn(console, 'error'); - var MyComp = React.createClass({ - propTypes: { - color: React.PropTypes.string, - }, - render: function() { - return React.createElement('div', null, 'My color is ' + this.color); - }, - }); - var ParentComp = React.createClass({ - render: function() { - return React.createElement(MyComp, {color: 123}); - }, - }); + function MyComp(props) { + return React.createElement('div', null, 'My color is ' + props.color); + } + MyComp.propTypes = { + color: PropTypes.string, + }; + function ParentComp() { + return React.createElement(MyComp, {color: 123}); + } ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + - 'expected `string`. Check the render method of `ParentComp`.' + expect(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 (created by ParentComp)\n' + + ' in ParentComp', ); @@ -248,3 +269,3 @@ describe('ReactElementValidator', function() { - it('gives a helpful error when passing null, undefined, boolean, or number', function() { + it('gives a helpful error when passing invalid types', () => { spyOn(console, 'error'); @@ -254,47 +275,59 @@ describe('ReactElementValidator', function() { React.createElement(123); - expect(console.error.calls.length).toBe(4); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + React.createElement({x: 17}); + React.createElement({}); + expect(console.error.calls.count()).toBe(6); + 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: undefined. You likely forgot to export your ' + + "component from the file it's defined in.", + ); + expect(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.', + ); + expect(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.', ); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + expect(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.', ); - expect(console.error.argsForCall[2][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + expect(console.error.calls.argsFor(4)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', ); - expect(console.error.argsForCall[3][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + expect(console.error.calls.argsFor(5)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object. You likely forgot to export your ' + + "component from the file it's defined in.", ); React.createElement('div'); - expect(console.error.calls.length).toBe(4); + expect(console.error.calls.count()).toBe(6); }); - it('includes the owner name when passing null, undefined, boolean, or number', function() { + it('includes the owner name when passing null, undefined, boolean, or number', () => { spyOn(console, 'error'); - var ParentComp = React.createClass({ - render: function() { - return React.createElement(null); - }, - }); + function ParentComp() { + return React.createElement(null); + } expect(function() { ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)); - }).toThrow( + }).toThrowError( 'Element 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`.' + 'or a class/function (for composite components) but got: null. Check ' + + 'the render method of `ParentComp`.', ); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components). Check the render method of ' + - '`ParentComp`.' + expect(console.error.calls.count()).toBe(1); + 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`.' + + '\n in ParentComp', ); @@ -302,14 +335,12 @@ describe('ReactElementValidator', function() { - it('should check default prop values', function() { + it('should check default prop values', () => { spyOn(console, 'error'); - var Component = React.createClass({ - propTypes: {prop: React.PropTypes.string.isRequired}, - getDefaultProps: function() { - return {prop: null}; - }, - render: function() { + class Component extends React.Component { + render() { return React.createElement('span', null, this.props.prop); - }, - }); + } + } + Component.propTypes = {prop: PropTypes.string.isRequired}; + Component.defaultProps = {prop: null}; @@ -317,6 +348,7 @@ describe('ReactElementValidator', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `Component`.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Failed prop type: The prop `prop` is marked as required in ' + + '`Component`, but its value is `null`.\n' + + ' in Component', ); @@ -324,23 +356,22 @@ describe('ReactElementValidator', function() { - it('should not check the default for explicit null', function() { + it('should not check the default for explicit null', () => { spyOn(console, 'error'); - var Component = React.createClass({ - propTypes: {prop: React.PropTypes.string.isRequired}, - getDefaultProps: function() { - return {prop: 'text'}; - }, - render: function() { + class Component extends React.Component { + render() { return React.createElement('span', null, this.props.prop); - }, - }); + } + } + Component.propTypes = {prop: PropTypes.string.isRequired}; + Component.defaultProps = {prop: 'text'}; ReactTestUtils.renderIntoDocument( - React.createElement(Component, {prop:null}) + React.createElement(Component, {prop: null}), ); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `Component`.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Failed prop type: The prop `prop` is marked as required in ' + + '`Component`, but its value is `null`.\n' + + ' in Component', ); @@ -348,31 +379,32 @@ describe('ReactElementValidator', function() { - it('should check declared prop types', function() { + it('should check declared prop types', () => { spyOn(console, 'error'); - var Component = React.createClass({ - propTypes: { - prop: React.PropTypes.string.isRequired, - }, - render: function() { + class Component extends React.Component { + render() { return React.createElement('span', null, this.props.prop); - }, - }); + } + } + Component.propTypes = { + prop: PropTypes.string.isRequired, + }; + ReactTestUtils.renderIntoDocument(React.createElement(Component)); ReactTestUtils.renderIntoDocument( - React.createElement(Component) - ); - ReactTestUtils.renderIntoDocument( - React.createElement(Component, {prop: 42}) + React.createElement(Component, {prop: 42}), ); - expect(console.error.calls.length).toBe(2); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `Component`.' + expect(console.error.calls.count()).toBe(2); + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Failed prop type: ' + + 'The prop `prop` is marked as required in `Component`, but its value ' + + 'is `undefined`.\n' + + ' in Component', ); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed propType: ' + - 'Invalid prop `prop` of type `number` supplied to ' + - '`Component`, expected `string`.' + expect(console.error.calls.argsFor(1)[0]).toBe( + 'Warning: Failed prop type: ' + + 'Invalid prop `prop` of type `number` supplied to ' + + '`Component`, expected `string`.\n' + + ' in Component', ); @@ -380,3 +412,3 @@ describe('ReactElementValidator', function() { ReactTestUtils.renderIntoDocument( - React.createElement(Component, {prop: 'string'}) + React.createElement(Component, {prop: 'string'}), ); @@ -384,28 +416,28 @@ describe('ReactElementValidator', function() { // Should not error for strings - expect(console.error.calls.length).toBe(2); + expect(console.error.calls.count()).toBe(2); }); - it('should warn if a PropType creator is used as a PropType', function() { + it('should warn if a PropType creator is used as a PropType', () => { spyOn(console, 'error'); - var Component = React.createClass({ - propTypes: { - myProp: React.PropTypes.shape, - }, - render: function() { + class Component extends React.Component { + render() { return React.createElement('span', null, this.props.myProp.value); - }, - }); + } + } + Component.propTypes = { + myProp: PropTypes.shape, + }; ReactTestUtils.renderIntoDocument( - React.createElement(Component, {myProp: {value: 'hi'}}) + React.createElement(Component, {myProp: {value: 'hi'}}), ); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Component: type specification of prop `myProp` is invalid; ' + - 'the type checker function must return `null` or an `Error` but ' + - 'returned a function. You may have forgotten to pass an argument to ' + - 'the type checker creator (arrayOf, instanceOf, objectOf, oneOf, ' + - 'oneOfType, and shape all require an argument).' + 'the type checker function must return `null` or an `Error` but ' + + 'returned a function. You may have forgotten to pass an argument to ' + + 'the type checker creator (arrayOf, instanceOf, objectOf, oneOf, ' + + 'oneOfType, and shape all require an argument).', ); @@ -413,15 +445,13 @@ describe('ReactElementValidator', function() { - it('should warn when accessing .type on an element factory', function() { - spyOn(console, 'error'); - var TestComponent = React.createClass({ - render: function() { - return
; - }, - }); + it('should warn when accessing .type on an element factory', () => { + spyOn(console, 'warn'); + function TestComponent() { + return
; + } var TestFactory = React.createFactory(TestComponent); expect(TestFactory.type).toBe(TestComponent); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.warn.calls.count()).toBe(1); + expect(console.warn.calls.argsFor(0)[0]).toBe( 'Warning: Factory.type is deprecated. Access the class directly before ' + - 'passing it to createFactory.' + 'passing it to createFactory.', ); @@ -429,15 +459,15 @@ describe('ReactElementValidator', function() { expect(TestFactory.type).toBe(TestComponent); - expect(console.error.argsForCall.length).toBe(1); + expect(console.warn.calls.count()).toBe(1); }); - it('does not warn when using DOM node as children', function() { + it('does not warn when using DOM node as children', () => { spyOn(console, 'error'); - var DOMContainer = React.createClass({ - render: function() { + class DOMContainer extends React.Component { + render() { return
; - }, - componentDidMount: function() { + } + componentDidMount() { ReactDOM.findDOMNode(this).appendChild(this.props.children); - }, - }); + } + } @@ -446,6 +476,6 @@ describe('ReactElementValidator', function() { ReactTestUtils.renderIntoDocument({node}); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('should not enumerate enumerable numbers (#4776)', function() { + it('should not enumerate enumerable numbers (#4776)', () => { /*eslint-disable no-extend-native */ @@ -469,3 +499,3 @@ describe('ReactElementValidator', function() { - it('does not blow up with inlined children', function() { + it('does not blow up with inlined children', () => { // We don't suggest this since it silences all sorts of warnings, but we @@ -474,3 +504,3 @@ describe('ReactElementValidator', function() { var child = { - $$typeof: (
).$$typeof, + $$typeof:
.$$typeof, type: 'span', @@ -485,3 +515,3 @@ describe('ReactElementValidator', function() { - it('does not blow up on key warning with undefined type', function() { + it('does not blow up on key warning with undefined type', () => { spyOn(console, 'error'); @@ -489,7 +519,8 @@ describe('ReactElementValidator', function() { void {[
]}; - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + expect(console.error.calls.count()).toBe(1); + 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. Check your code at **.", ); @@ -497,2 +528,52 @@ describe('ReactElementValidator', function() { + it('provides stack via non-standard console.reactStack for invalid types', () => { + spyOn(console, 'error'); + + function Foo() { + var Bad = undefined; + return React.createElement(Bad); + } + + function App() { + return React.createElement('div', null, React.createElement(Foo)); + } + + try { + console.reactStack = jest.fn(); + console.reactStackEnd = jest.fn(); + + expect(() => { + ReactTestUtils.renderIntoDocument(React.createElement(App)); + }).toThrow( + 'Element 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. Check the render method of `Foo`.', + ); + + expect(console.reactStack.mock.calls.length).toBe(1); + expect(console.reactStackEnd.mock.calls.length).toBe(1); + + var stack = console.reactStack.mock.calls[0][0]; + expect(Array.isArray(stack)).toBe(true); + expect(stack.map(frame => frame.name)).toEqual([ + 'Foo', // is inside Foo + 'App', // is inside App + 'App', //
is inside App + null, // is outside a component + ]); + expect( + stack.map(frame => frame.fileName && frame.fileName.slice(-8)), + ).toEqual([null, null, null, null]); + expect(stack.map(frame => frame.lineNumber)).toEqual([ + null, + null, + null, + null, + ]); + } finally { + delete console.reactStack; + delete console.reactStackEnd; + } + }); }); diff --git a/src/isomorphic/classic/types/ReactPropTypes.js b/src/isomorphic/classic/types/ReactPropTypes.js index 99c036496..5c0ca7735 100644 --- a/src/isomorphic/classic/types/ReactPropTypes.js +++ b/src/isomorphic/classic/types/ReactPropTypes.js @@ -13,434 +13,5 @@ -var ReactElement = require('ReactElement'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); +var {isValidElement} = require('ReactElement'); +var factory = require('prop-types/factory'); -var emptyFunction = require('emptyFunction'); -var getIteratorFn = require('getIteratorFn'); - -/** - * Collection of methods that allow declaration and validation of props that are - * supplied to React components. Example usage: - * - * var Props = require('ReactPropTypes'); - * var MyArticle = React.createClass({ - * propTypes: { - * // An optional string prop named "description". - * description: Props.string, - * - * // A required enum prop named "category". - * category: Props.oneOf(['News','Photos']).isRequired, - * - * // A prop named "dialog" that requires an instance of Dialog. - * dialog: Props.instanceOf(Dialog).isRequired - * }, - * render: function() { ... } - * }); - * - * A more formal specification of how these methods are used: - * - * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...) - * decl := ReactPropTypes.{type}(.isRequired)? - * - * Each and every declaration produces a function with the same signature. This - * allows the creation of custom validation functions. For example: - * - * var MyLink = React.createClass({ - * propTypes: { - * // An optional string or URI prop named "href". - * href: function(props, propName, componentName) { - * var propValue = props[propName]; - * if (propValue != null && typeof propValue !== 'string' && - * !(propValue instanceof URI)) { - * return new Error( - * 'Expected a string or an URI for ' + propName + ' in ' + - * componentName - * ); - * } - * } - * }, - * render: function() {...} - * }); - * - * @internal - */ - -var ANONYMOUS = '<>'; - -var ReactPropTypes = { - array: createPrimitiveTypeChecker('array'), - bool: createPrimitiveTypeChecker('boolean'), - func: createPrimitiveTypeChecker('function'), - number: createPrimitiveTypeChecker('number'), - object: createPrimitiveTypeChecker('object'), - string: createPrimitiveTypeChecker('string'), - - any: createAnyTypeChecker(), - arrayOf: createArrayOfTypeChecker, - element: createElementTypeChecker(), - instanceOf: createInstanceTypeChecker, - node: createNodeChecker(), - objectOf: createObjectOfTypeChecker, - oneOf: createEnumTypeChecker, - oneOfType: createUnionTypeChecker, - shape: createShapeTypeChecker, -}; - -/** - * inlined Object.is polyfill to avoid requiring consumers ship their own - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is - */ -/*eslint-disable no-self-compare*/ -function is(x, y) { - // SameValue algorithm - if (x === y) { // Steps 1-5, 7-10 - // Steps 6.b-6.e: +0 != -0 - return x !== 0 || 1 / x === 1 / y; - } else { - // Step 6.a: NaN == NaN - return x !== x && y !== y; - } -} -/*eslint-enable no-self-compare*/ - -function createChainableTypeChecker(validate) { - function checkType( - isRequired, - props, - propName, - componentName, - location, - propFullName - ) { - componentName = componentName || ANONYMOUS; - propFullName = propFullName || propName; - if (props[propName] == null) { - var locationName = ReactPropTypeLocationNames[location]; - if (isRequired) { - return new Error( - `Required ${locationName} \`${propFullName}\` was not specified in ` + - `\`${componentName}\`.` - ); - } - return null; - } else { - return validate(props, propName, componentName, location, propFullName); - } - } - - var chainedCheckType = checkType.bind(null, false); - chainedCheckType.isRequired = checkType.bind(null, true); - - return chainedCheckType; -} - -function createPrimitiveTypeChecker(expectedType) { - function validate(props, propName, componentName, location, propFullName) { - var propValue = props[propName]; - var propType = getPropType(propValue); - if (propType !== expectedType) { - var locationName = ReactPropTypeLocationNames[location]; - // `propValue` being instance of, say, date/regexp, pass the 'object' - // check, but we can offer a more precise error message here rather than - // 'of type `object`'. - var preciseType = getPreciseType(propValue); - - return new Error( - `Invalid ${locationName} \`${propFullName}\` of type ` + - `\`${preciseType}\` supplied to \`${componentName}\`, expected ` + - `\`${expectedType}\`.` - ); - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createAnyTypeChecker() { - return createChainableTypeChecker(emptyFunction.thatReturns(null)); -} - -function createArrayOfTypeChecker(typeChecker) { - function validate(props, propName, componentName, location, propFullName) { - if (typeof typeChecker !== 'function') { - return new Error( - `Property \`${propFullName}\` of component \`${componentName}\` has invalid PropType notation inside arrayOf.` - ); - } - var propValue = props[propName]; - if (!Array.isArray(propValue)) { - var locationName = ReactPropTypeLocationNames[location]; - var propType = getPropType(propValue); - return new Error( - `Invalid ${locationName} \`${propFullName}\` of type ` + - `\`${propType}\` supplied to \`${componentName}\`, expected an array.` - ); - } - for (var i = 0; i < propValue.length; i++) { - var error = typeChecker( - propValue, - i, - componentName, - location, - `${propFullName}[${i}]` - ); - if (error instanceof Error) { - return error; - } - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createElementTypeChecker() { - function validate(props, propName, componentName, location, propFullName) { - if (!ReactElement.isValidElement(props[propName])) { - var locationName = ReactPropTypeLocationNames[location]; - return new Error( - `Invalid ${locationName} \`${propFullName}\` supplied to ` + - `\`${componentName}\`, expected a single ReactElement.` - ); - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createInstanceTypeChecker(expectedClass) { - function validate(props, propName, componentName, location, propFullName) { - if (!(props[propName] instanceof expectedClass)) { - var locationName = ReactPropTypeLocationNames[location]; - var expectedClassName = expectedClass.name || ANONYMOUS; - var actualClassName = getClassName(props[propName]); - return new Error( - `Invalid ${locationName} \`${propFullName}\` of type ` + - `\`${actualClassName}\` supplied to \`${componentName}\`, expected ` + - `instance of \`${expectedClassName}\`.` - ); - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createEnumTypeChecker(expectedValues) { - if (!Array.isArray(expectedValues)) { - return createChainableTypeChecker(function() { - return new Error( - `Invalid argument supplied to oneOf, expected an instance of array.` - ); - }); - } - - function validate(props, propName, componentName, location, propFullName) { - var propValue = props[propName]; - for (var i = 0; i < expectedValues.length; i++) { - if (is(propValue, expectedValues[i])) { - return null; - } - } - - var locationName = ReactPropTypeLocationNames[location]; - var valuesString = JSON.stringify(expectedValues); - return new Error( - `Invalid ${locationName} \`${propFullName}\` of value \`${propValue}\` ` + - `supplied to \`${componentName}\`, expected one of ${valuesString}.` - ); - } - return createChainableTypeChecker(validate); -} - -function createObjectOfTypeChecker(typeChecker) { - function validate(props, propName, componentName, location, propFullName) { - if (typeof typeChecker !== 'function') { - return new Error( - `Property \`${propFullName}\` of component \`${componentName}\` has invalid PropType notation inside objectOf.` - ); - } - var propValue = props[propName]; - var propType = getPropType(propValue); - if (propType !== 'object') { - var locationName = ReactPropTypeLocationNames[location]; - return new Error( - `Invalid ${locationName} \`${propFullName}\` of type ` + - `\`${propType}\` supplied to \`${componentName}\`, expected an object.` - ); - } - for (var key in propValue) { - if (propValue.hasOwnProperty(key)) { - var error = typeChecker( - propValue, - key, - componentName, - location, - `${propFullName}.${key}` - ); - if (error instanceof Error) { - return error; - } - } - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createUnionTypeChecker(arrayOfTypeCheckers) { - if (!Array.isArray(arrayOfTypeCheckers)) { - return createChainableTypeChecker(function() { - return new Error( - `Invalid argument supplied to oneOfType, expected an instance of array.` - ); - }); - } - - function validate(props, propName, componentName, location, propFullName) { - for (var i = 0; i < arrayOfTypeCheckers.length; i++) { - var checker = arrayOfTypeCheckers[i]; - if ( - checker(props, propName, componentName, location, propFullName) == null - ) { - return null; - } - } - - var locationName = ReactPropTypeLocationNames[location]; - return new Error( - `Invalid ${locationName} \`${propFullName}\` supplied to ` + - `\`${componentName}\`.` - ); - } - return createChainableTypeChecker(validate); -} - -function createNodeChecker() { - function validate(props, propName, componentName, location, propFullName) { - if (!isNode(props[propName])) { - var locationName = ReactPropTypeLocationNames[location]; - return new Error( - `Invalid ${locationName} \`${propFullName}\` supplied to ` + - `\`${componentName}\`, expected a ReactNode.` - ); - } - return null; - } - return createChainableTypeChecker(validate); -} - -function createShapeTypeChecker(shapeTypes) { - function validate(props, propName, componentName, location, propFullName) { - var propValue = props[propName]; - var propType = getPropType(propValue); - if (propType !== 'object') { - var locationName = ReactPropTypeLocationNames[location]; - return new Error( - `Invalid ${locationName} \`${propFullName}\` of type \`${propType}\` ` + - `supplied to \`${componentName}\`, expected \`object\`.` - ); - } - for (var key in shapeTypes) { - var checker = shapeTypes[key]; - if (!checker) { - continue; - } - var error = checker( - propValue, - key, - componentName, - location, - `${propFullName}.${key}` - ); - if (error) { - return error; - } - } - return null; - } - return createChainableTypeChecker(validate); -} - -function isNode(propValue) { - switch (typeof propValue) { - case 'number': - case 'string': - case 'undefined': - return true; - case 'boolean': - return !propValue; - case 'object': - if (Array.isArray(propValue)) { - return propValue.every(isNode); - } - if (propValue === null || ReactElement.isValidElement(propValue)) { - return true; - } - - var iteratorFn = getIteratorFn(propValue); - if (iteratorFn) { - var iterator = iteratorFn.call(propValue); - var step; - if (iteratorFn !== propValue.entries) { - while (!(step = iterator.next()).done) { - if (!isNode(step.value)) { - return false; - } - } - } else { - // Iterator will provide entry [k,v] tuples rather than values. - while (!(step = iterator.next()).done) { - var entry = step.value; - if (entry) { - if (!isNode(entry[1])) { - return false; - } - } - } - } - } else { - return false; - } - - return true; - default: - return false; - } -} - -// Equivalent of `typeof` but with special handling for array and regexp. -function getPropType(propValue) { - var propType = typeof propValue; - if (Array.isArray(propValue)) { - return 'array'; - } - if (propValue instanceof RegExp) { - // Old webkits (at least until Android 4.0) return 'function' rather than - // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ - // passes PropTypes.object. - return 'object'; - } - return propType; -} - -// This handles more types than `getPropType`. Only used for error messages. -// See `createPrimitiveTypeChecker`. -function getPreciseType(propValue) { - var propType = getPropType(propValue); - if (propType === 'object') { - if (propValue instanceof Date) { - return 'date'; - } else if (propValue instanceof RegExp) { - return 'regexp'; - } - } - return propType; -} - -// Returns class name of the object, if any. -function getClassName(propValue) { - if (!propValue.constructor || !propValue.constructor.name) { - return ANONYMOUS; - } - return propValue.constructor.name; -} - -module.exports = ReactPropTypes; +module.exports = factory(isValidElement); diff --git a/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js b/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js index 9601e675b..c12cc1c29 100644 --- a/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js +++ b/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js @@ -16,4 +16,4 @@ var React; var ReactFragment; -var ReactPropTypeLocations; var ReactTestUtils; +var ReactPropTypesSecret; @@ -21,4 +21,2 @@ var Component; var MyComponent; -var requiredMessage = - 'Required prop `testProp` was not specified in `testComponent`.'; @@ -30,3 +28,5 @@ function typeCheckFail(declaration, value, message) { 'testComponent', - ReactPropTypeLocations.prop + 'prop', + null, + ReactPropTypesSecret, ); @@ -36,2 +36,44 @@ function typeCheckFail(declaration, value, message) { +function typeCheckFailRequiredValues(declaration) { + var specifiedButIsNullMsg = + 'The prop `testProp` is marked as required in ' + + '`testComponent`, but its value is `null`.'; + var unspecifiedMsg = + 'The prop `testProp` is marked as required in ' + + '`testComponent`, but its value is `undefined`.'; + var props1 = {testProp: null}; + var error1 = declaration( + props1, + 'testProp', + 'testComponent', + 'prop', + null, + ReactPropTypesSecret, + ); + expect(error1 instanceof Error).toBe(true); + expect(error1.message).toBe(specifiedButIsNullMsg); + var props2 = {testProp: undefined}; + var error2 = declaration( + props2, + 'testProp', + 'testComponent', + 'prop', + null, + ReactPropTypesSecret, + ); + expect(error2 instanceof Error).toBe(true); + expect(error2.message).toBe(unspecifiedMsg); + var props3 = {}; + var error3 = declaration( + props3, + 'testProp', + 'testComponent', + 'prop', + null, + ReactPropTypesSecret, + ); + expect(error3 instanceof Error).toBe(true); + expect(error3.message).toBe(unspecifiedMsg); +} + function typeCheckPass(declaration, value) { @@ -42,3 +84,5 @@ function typeCheckPass(declaration, value) { 'testComponent', - ReactPropTypeLocations.prop + 'prop', + null, + ReactPropTypesSecret, ); @@ -47,13 +91,33 @@ function typeCheckPass(declaration, value) { -describe('ReactPropTypes', function() { - beforeEach(function() { - PropTypes = require('ReactPropTypes'); - React = require('React'); - ReactFragment = require('ReactFragment'); - ReactPropTypeLocations = require('ReactPropTypeLocations'); - ReactTestUtils = require('ReactTestUtils'); +function resetWarningCache() { + jest.resetModules(); + PropTypes = require('ReactPropTypes'); + React = require('React'); + ReactFragment = require('ReactFragment'); + ReactTestUtils = require('ReactTestUtils'); + ReactPropTypesSecret = require('ReactPropTypesSecret'); +} + +function expectWarningInDevelopment(declaration, value) { + resetWarningCache(); + var props = {testProp: value}; + var propName = 'testProp' + Math.random().toString(); + var componentName = 'testComponent' + Math.random().toString(); + for (var i = 0; i < 3; i++) { + declaration(props, propName, componentName, 'prop'); + } + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'You are manually calling a React.PropTypes validation ', + ); + console.error.calls.reset(); +} + +describe('ReactPropTypes', () => { + beforeEach(() => { + resetWarningCache(); }); - describe('Primitive Types', function() { - it('should warn for invalid strings', function() { + describe('Primitive Types', () => { + it('should warn for invalid strings', () => { typeCheckFail( @@ -62,3 +126,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `array` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', ); @@ -68,3 +132,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `boolean` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', ); @@ -74,3 +138,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `number` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', ); @@ -80,3 +144,9 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `object` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', + ); + typeCheckFail( + PropTypes.string, + Symbol(), + 'Invalid prop `testProp` of type `symbol` supplied to ' + + '`testComponent`, expected `string`.', ); @@ -84,3 +154,3 @@ describe('ReactPropTypes', function() { - it('should fail date and regexp correctly', function() { + it('should fail date and regexp correctly', () => { typeCheckFail( @@ -89,3 +159,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `date` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', ); @@ -95,3 +165,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `regexp` supplied to ' + - '`testComponent`, expected `string`.' + '`testComponent`, expected `string`.', ); @@ -99,3 +169,3 @@ describe('ReactPropTypes', function() { - it('should not warn for valid values', function() { + it('should not warn for valid values', () => { typeCheckPass(PropTypes.array, []); @@ -108,5 +178,6 @@ describe('ReactPropTypes', function() { typeCheckPass(PropTypes.object, /please/); + typeCheckPass(PropTypes.symbol, Symbol()); }); - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.string, null); @@ -115,5 +186,50 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail(PropTypes.string.isRequired, null, requiredMessage); - typeCheckFail(PropTypes.string.isRequired, undefined, requiredMessage); + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.string.isRequired); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.array, /please/); + expectWarningInDevelopment(PropTypes.array, []); + expectWarningInDevelopment(PropTypes.array.isRequired, /please/); + expectWarningInDevelopment(PropTypes.array.isRequired, []); + expectWarningInDevelopment(PropTypes.array.isRequired, null); + expectWarningInDevelopment(PropTypes.array.isRequired, undefined); + expectWarningInDevelopment(PropTypes.bool, []); + expectWarningInDevelopment(PropTypes.bool, true); + expectWarningInDevelopment(PropTypes.bool.isRequired, []); + expectWarningInDevelopment(PropTypes.bool.isRequired, true); + expectWarningInDevelopment(PropTypes.bool.isRequired, null); + expectWarningInDevelopment(PropTypes.bool.isRequired, undefined); + expectWarningInDevelopment(PropTypes.func, false); + expectWarningInDevelopment(PropTypes.func, function() {}); + expectWarningInDevelopment(PropTypes.func.isRequired, false); + expectWarningInDevelopment(PropTypes.func.isRequired, function() {}); + expectWarningInDevelopment(PropTypes.func.isRequired, null); + expectWarningInDevelopment(PropTypes.func.isRequired, undefined); + expectWarningInDevelopment(PropTypes.number, function() {}); + expectWarningInDevelopment(PropTypes.number, 42); + expectWarningInDevelopment(PropTypes.number.isRequired, function() {}); + expectWarningInDevelopment(PropTypes.number.isRequired, 42); + expectWarningInDevelopment(PropTypes.number.isRequired, null); + expectWarningInDevelopment(PropTypes.number.isRequired, undefined); + expectWarningInDevelopment(PropTypes.string, 0); + expectWarningInDevelopment(PropTypes.string, 'foo'); + expectWarningInDevelopment(PropTypes.string.isRequired, 0); + expectWarningInDevelopment(PropTypes.string.isRequired, 'foo'); + expectWarningInDevelopment(PropTypes.string.isRequired, null); + expectWarningInDevelopment(PropTypes.string.isRequired, undefined); + expectWarningInDevelopment(PropTypes.symbol, 0); + expectWarningInDevelopment(PropTypes.symbol, Symbol('Foo')); + expectWarningInDevelopment(PropTypes.symbol.isRequired, 0); + expectWarningInDevelopment(PropTypes.symbol.isRequired, Symbol('Foo')); + expectWarningInDevelopment(PropTypes.symbol.isRequired, null); + expectWarningInDevelopment(PropTypes.symbol.isRequired, undefined); + expectWarningInDevelopment(PropTypes.object, ''); + expectWarningInDevelopment(PropTypes.object, {foo: 'bar'}); + expectWarningInDevelopment(PropTypes.object.isRequired, ''); + expectWarningInDevelopment(PropTypes.object.isRequired, {foo: 'bar'}); + expectWarningInDevelopment(PropTypes.object.isRequired, null); + expectWarningInDevelopment(PropTypes.object.isRequired, undefined); }); @@ -121,4 +237,4 @@ describe('ReactPropTypes', function() { - describe('Any type', function() { - it('should should accept any value', function() { + describe('Any type', () => { + it('should should accept any value', () => { typeCheckPass(PropTypes.any, 0); @@ -126,5 +242,6 @@ describe('ReactPropTypes', function() { typeCheckPass(PropTypes.any, []); + typeCheckPass(PropTypes.any, Symbol()); }); - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.any, null); @@ -133,5 +250,11 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail(PropTypes.any.isRequired, null, requiredMessage); - typeCheckFail(PropTypes.any.isRequired, undefined, requiredMessage); + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.any.isRequired); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.any, null); + expectWarningInDevelopment(PropTypes.any.isRequired, null); + expectWarningInDevelopment(PropTypes.any.isRequired, undefined); }); @@ -139,8 +262,8 @@ describe('ReactPropTypes', function() { - describe('ArrayOf Type', function() { - it('should fail for invalid argument', function() { + describe('ArrayOf Type', () => { + it('should fail for invalid argument', () => { typeCheckFail( - PropTypes.arrayOf({ foo: PropTypes.string }), - { foo: 'bar' }, - 'Property `testProp` of component `testComponent` has invalid PropType notation inside arrayOf.' + PropTypes.arrayOf({foo: PropTypes.string}), + {foo: 'bar'}, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside arrayOf.', ); @@ -148,3 +271,3 @@ describe('ReactPropTypes', function() { - it('should support the arrayOf propTypes', function() { + it('should support the arrayOf propTypes', () => { typeCheckPass(PropTypes.arrayOf(PropTypes.number), [1, 2, 3]); @@ -152,8 +275,9 @@ describe('ReactPropTypes', function() { typeCheckPass(PropTypes.arrayOf(PropTypes.oneOf(['a', 'b'])), ['a', 'b']); + typeCheckPass(PropTypes.arrayOf(PropTypes.symbol), [Symbol(), Symbol()]); }); - it('should support arrayOf with complex types', function() { + it('should support arrayOf with complex types', () => { typeCheckPass( PropTypes.arrayOf(PropTypes.shape({a: PropTypes.number.isRequired})), - [{a: 1}, {a: 2}] + [{a: 1}, {a: 2}], ); @@ -161,9 +285,9 @@ describe('ReactPropTypes', function() { function Thing() {} - typeCheckPass( - PropTypes.arrayOf(PropTypes.instanceOf(Thing)), - [new Thing(), new Thing()] - ); + typeCheckPass(PropTypes.arrayOf(PropTypes.instanceOf(Thing)), [ + new Thing(), + new Thing(), + ]); }); - it('should warn with invalid items in the array', function() { + it('should warn with invalid items in the array', () => { typeCheckFail( @@ -172,3 +296,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp[2]` of type `string` supplied to ' + - '`testComponent`, expected `number`.' + '`testComponent`, expected `number`.', ); @@ -176,3 +300,3 @@ describe('ReactPropTypes', function() { - it('should warn with invalid complex types', function() { + it('should warn with invalid complex types', () => { function Thing() {} @@ -184,3 +308,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp[1]` of type `String` supplied to ' + - '`testComponent`, expected instance of `' + name + '`.' + '`testComponent`, expected instance of `' + + name + + '`.', ); @@ -188,3 +314,3 @@ describe('ReactPropTypes', function() { - it('should warn when passed something other than an array', function() { + it('should warn when passed something other than an array', () => { typeCheckFail( @@ -193,3 +319,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `object` supplied to ' + - '`testComponent`, expected an array.' + '`testComponent`, expected an array.', ); @@ -199,3 +325,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `number` supplied to ' + - '`testComponent`, expected an array.' + '`testComponent`, expected an array.', ); @@ -205,3 +331,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `string` supplied to ' + - '`testComponent`, expected an array.' + '`testComponent`, expected an array.', ); @@ -209,3 +335,3 @@ describe('ReactPropTypes', function() { - it('should not warn when passing an empty array', function() { + it('should not warn when passing an empty array', () => { typeCheckPass(PropTypes.arrayOf(PropTypes.number), []); @@ -213,3 +339,3 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.arrayOf(PropTypes.number), null); @@ -218,12 +344,29 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.arrayOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.arrayOf({foo: PropTypes.string}), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.arrayOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectWarningInDevelopment(PropTypes.arrayOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( PropTypes.arrayOf(PropTypes.number).isRequired, null, - requiredMessage ); - typeCheckFail( + expectWarningInDevelopment( PropTypes.arrayOf(PropTypes.number).isRequired, undefined, - requiredMessage ); @@ -232,13 +375,13 @@ describe('ReactPropTypes', function() { - describe('Component Type', function() { - beforeEach(function() { - Component = React.createClass({ - propTypes: { + describe('Component Type', () => { + beforeEach(() => { + Component = class extends React.Component { + static propTypes = { label: PropTypes.element.isRequired, - }, + }; - render: function() { + render() { return
{this.props.label}
; - }, - }); + } + }; }); @@ -250,8 +393,26 @@ describe('ReactPropTypes', function() { it('should not support multiple components or scalar values', () => { - var message = 'Invalid prop `testProp` supplied to `testComponent`, ' + - 'expected a single ReactElement.'; - typeCheckFail(PropTypes.element, [
,
], message); - typeCheckFail(PropTypes.element, 123, message); - typeCheckFail(PropTypes.element, 'foo', message); - typeCheckFail(PropTypes.element, false, message); + typeCheckFail( + PropTypes.element, + [
,
], + 'Invalid prop `testProp` of type `array` supplied to `testComponent`, ' + + 'expected a single ReactElement.', + ); + typeCheckFail( + PropTypes.element, + 123, + 'Invalid prop `testProp` of type `number` supplied to `testComponent`, ' + + 'expected a single ReactElement.', + ); + typeCheckFail( + PropTypes.element, + 'foo', + 'Invalid prop `testProp` of type `string` supplied to `testComponent`, ' + + 'expected a single ReactElement.', + ); + typeCheckFail( + PropTypes.element, + false, + 'Invalid prop `testProp` of type `boolean` supplied to `testComponent`, ' + + 'expected a single ReactElement.', + ); }); @@ -262,5 +423,5 @@ describe('ReactPropTypes', function() { var instance = } />; - instance = ReactTestUtils.renderIntoDocument(instance); + ReactTestUtils.renderIntoDocument(instance); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -271,8 +432,8 @@ describe('ReactPropTypes', function() { var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); + ReactTestUtils.renderIntoDocument(instance); - expect(console.error.argsForCall.length).toBe(1); + expect(console.error.calls.count()).toBe(1); }); - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.element, null); @@ -281,5 +442,15 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail(PropTypes.element.isRequired, null, requiredMessage); - typeCheckFail(PropTypes.element.isRequired, undefined, requiredMessage); + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.element.isRequired); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.element, [
,
]); + expectWarningInDevelopment(PropTypes.element,
); + expectWarningInDevelopment(PropTypes.element, 123); + expectWarningInDevelopment(PropTypes.element, 'foo'); + expectWarningInDevelopment(PropTypes.element, false); + expectWarningInDevelopment(PropTypes.element.isRequired, null); + expectWarningInDevelopment(PropTypes.element.isRequired, undefined); }); @@ -287,4 +458,4 @@ describe('ReactPropTypes', function() { - describe('Instance Types', function() { - it('should warn for invalid instances', function() { + describe('Instance Types', () => { + it('should warn for invalid instances', () => { function Person() {} @@ -299,3 +470,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `Boolean` supplied to ' + - '`testComponent`, expected instance of `' + personName + '`.' + '`testComponent`, expected instance of `' + + personName + + '`.', ); @@ -305,3 +478,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `Object` supplied to ' + - '`testComponent`, expected instance of `' + personName + '`.' + '`testComponent`, expected instance of `' + + personName + + '`.', ); @@ -311,3 +486,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `String` supplied to ' + - '`testComponent`, expected instance of `' + personName + '`.' + '`testComponent`, expected instance of `' + + personName + + '`.', ); @@ -317,3 +494,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `Object` supplied to ' + - '`testComponent`, expected instance of `' + dateName + '`.' + '`testComponent`, expected instance of `' + + dateName + + '`.', ); @@ -323,3 +502,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `Object` supplied to ' + - '`testComponent`, expected instance of `' + regExpName + '`.' + '`testComponent`, expected instance of `' + + regExpName + + '`.', ); @@ -329,3 +510,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `Cat` supplied to ' + - '`testComponent`, expected instance of `' + personName + '`.' + '`testComponent`, expected instance of `' + + personName + + '`.', ); @@ -335,3 +518,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `<>` supplied to ' + - '`testComponent`, expected instance of `' + personName + '`.' + '`testComponent`, expected instance of `' + + personName + + '`.', ); @@ -339,3 +524,3 @@ describe('ReactPropTypes', function() { - it('should not warn for valid values', function() { + it('should not warn for valid values', () => { function Person() {} @@ -351,3 +536,3 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.instanceOf(String), null); @@ -356,8 +541,14 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( - PropTypes.instanceOf(String).isRequired, null, requiredMessage - ); - typeCheckFail( - PropTypes.instanceOf(String).isRequired, undefined, requiredMessage + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.instanceOf(String).isRequired); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.instanceOf(Date), {}); + expectWarningInDevelopment(PropTypes.instanceOf(Date), new Date()); + expectWarningInDevelopment(PropTypes.instanceOf(Date).isRequired, {}); + expectWarningInDevelopment( + PropTypes.instanceOf(Date).isRequired, + new Date(), ); @@ -366,13 +557,14 @@ describe('ReactPropTypes', function() { - describe('React Component Types', function() { - beforeEach(function() { - MyComponent = React.createClass({ - render: function() { + describe('React Component Types', () => { + beforeEach(() => { + MyComponent = class extends React.Component { + render() { return
; - }, - }); + } + }; }); - it('should warn for invalid values', function() { - var failMessage = 'Invalid prop `testProp` supplied to ' + + it('should warn for invalid values', () => { + var failMessage = + 'Invalid prop `testProp` supplied to ' + '`testComponent`, expected a ReactNode.'; @@ -384,3 +576,3 @@ describe('ReactPropTypes', function() { - it('should not warn for valid values', function() { + it('should not warn for valid values', () => { spyOn(console, 'error'); @@ -402,18 +594,21 @@ describe('ReactPropTypes', function() { var frag = ReactFragment.create; - typeCheckPass(PropTypes.node, frag({ - k0: 123, - k1: 'Some string', - k2:
, - k3: frag({ - k30: , - k31: frag({k310: }), - k32: 'Another string', + typeCheckPass( + PropTypes.node, + frag({ + k0: 123, + k1: 'Some string', + k2:
, + k3: frag({ + k30: , + k31: frag({k310: }), + k32: 'Another string', + }), + k4: null, + k5: undefined, }), - k4: null, - k5: undefined, - })); - expect(console.error.calls).toEqual([]); + ); + expect(console.error.calls.count()).toBe(0); }); - it('should not warn for iterables', function() { + it('should not warn for iterables', () => { var iterable = { @@ -433,3 +628,3 @@ describe('ReactPropTypes', function() { - it('should not warn for entry iterables', function() { + it('should not warn for entry iterables', () => { var iterable = { @@ -440,3 +635,6 @@ describe('ReactPropTypes', function() { var done = ++i > 2; - return {value: done ? undefined : ['#' + i, ], done: done}; + return { + value: done ? undefined : ['#' + i, ], + done: done, + }; }, @@ -450,3 +648,3 @@ describe('ReactPropTypes', function() { - it('should not warn for null/undefined if not required', function() { + it('should not warn for null/undefined if not required', () => { typeCheckPass(PropTypes.node, null); @@ -455,26 +653,26 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( - PropTypes.node.isRequired, - null, - 'Required prop `testProp` was not specified in `testComponent`.' - ); - typeCheckFail( - PropTypes.node.isRequired, - undefined, - 'Required prop `testProp` was not specified in `testComponent`.' - ); + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.node.isRequired); }); - it('should accept empty array for required props', function() { + it('should accept empty array for required props', () => { typeCheckPass(PropTypes.node.isRequired, []); }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.node, 'node'); + expectWarningInDevelopment(PropTypes.node, {}); + expectWarningInDevelopment(PropTypes.node.isRequired, 'node'); + expectWarningInDevelopment(PropTypes.node.isRequired, undefined); + expectWarningInDevelopment(PropTypes.node.isRequired, undefined); + }); }); - describe('ObjectOf Type', function() { - it('should fail for invalid argument', function() { + describe('ObjectOf Type', () => { + it('should fail for invalid argument', () => { typeCheckFail( - PropTypes.objectOf({ foo: PropTypes.string }), - { foo: 'bar' }, - 'Property `testProp` of component `testComponent` has invalid PropType notation inside objectOf.' + PropTypes.objectOf({foo: PropTypes.string}), + {foo: 'bar'}, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside objectOf.', ); @@ -482,18 +680,24 @@ describe('ReactPropTypes', function() { - it('should support the objectOf propTypes', function() { + it('should support the objectOf propTypes', () => { typeCheckPass(PropTypes.objectOf(PropTypes.number), {a: 1, b: 2, c: 3}); - typeCheckPass( - PropTypes.objectOf(PropTypes.string), - {a: 'a', b: 'b', c: 'c'} - ); - typeCheckPass( - PropTypes.objectOf(PropTypes.oneOf(['a', 'b'])), - {a: 'a', b: 'b'} - ); + typeCheckPass(PropTypes.objectOf(PropTypes.string), { + a: 'a', + b: 'b', + c: 'c', + }); + typeCheckPass(PropTypes.objectOf(PropTypes.oneOf(['a', 'b'])), { + a: 'a', + b: 'b', + }); + typeCheckPass(PropTypes.objectOf(PropTypes.symbol), { + a: Symbol(), + b: Symbol(), + c: Symbol(), + }); }); - it('should support objectOf with complex types', function() { + it('should support objectOf with complex types', () => { typeCheckPass( PropTypes.objectOf(PropTypes.shape({a: PropTypes.number.isRequired})), - {a: {a: 1}, b: {a: 2}} + {a: {a: 1}, b: {a: 2}}, ); @@ -501,9 +705,9 @@ describe('ReactPropTypes', function() { function Thing() {} - typeCheckPass( - PropTypes.objectOf(PropTypes.instanceOf(Thing)), - {a: new Thing(), b: new Thing()} - ); + typeCheckPass(PropTypes.objectOf(PropTypes.instanceOf(Thing)), { + a: new Thing(), + b: new Thing(), + }); }); - it('should warn with invalid items in the object', function() { + it('should warn with invalid items in the object', () => { typeCheckFail( @@ -512,3 +716,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp.c` of type `string` supplied to `testComponent`, ' + - 'expected `number`.' + 'expected `number`.', ); @@ -516,3 +720,3 @@ describe('ReactPropTypes', function() { - it('should warn with invalid complex types', function() { + it('should warn with invalid complex types', () => { function Thing() {} @@ -524,3 +728,5 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp.b` of type `String` supplied to ' + - '`testComponent`, expected instance of `' + name + '`.' + '`testComponent`, expected instance of `' + + name + + '`.', ); @@ -528,3 +734,3 @@ describe('ReactPropTypes', function() { - it('should warn when passed something other than an object', function() { + it('should warn when passed something other than an object', () => { typeCheckFail( @@ -533,3 +739,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `array` supplied to ' + - '`testComponent`, expected an object.' + '`testComponent`, expected an object.', ); @@ -539,3 +745,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `number` supplied to ' + - '`testComponent`, expected an object.' + '`testComponent`, expected an object.', ); @@ -545,3 +751,9 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `string` supplied to ' + - '`testComponent`, expected an object.' + '`testComponent`, expected an object.', + ); + typeCheckFail( + PropTypes.objectOf(PropTypes.symbol), + Symbol(), + 'Invalid prop `testProp` of type `symbol` supplied to ' + + '`testComponent`, expected an object.', ); @@ -549,3 +761,3 @@ describe('ReactPropTypes', function() { - it('should not warn when passing an empty object', function() { + it('should not warn when passing an empty object', () => { typeCheckPass(PropTypes.objectOf(PropTypes.number), {}); @@ -553,3 +765,3 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.objectOf(PropTypes.number), null); @@ -558,12 +770,23 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( PropTypes.objectOf(PropTypes.number).isRequired, - null, - requiredMessage ); - typeCheckFail( - PropTypes.objectOf(PropTypes.number).isRequired, + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.objectOf({foo: PropTypes.string}), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.objectOf(PropTypes.number), { + a: 1, + b: 2, + c: 'b', + }); + expectWarningInDevelopment(PropTypes.objectOf(PropTypes.number), [1, 2]); + expectWarningInDevelopment(PropTypes.objectOf(PropTypes.number), null); + expectWarningInDevelopment( + PropTypes.objectOf(PropTypes.number), undefined, - requiredMessage ); @@ -572,12 +795,17 @@ describe('ReactPropTypes', function() { - describe('OneOf Types', function() { - it('should fail for invalid argument', function() { - typeCheckFail( - PropTypes.oneOf('red', 'blue'), - 'red', - 'Invalid argument supplied to oneOf, expected an instance of array.' + describe('OneOf Types', () => { + it('should warn but not error for invalid argument', () => { + spyOn(console, 'error'); + + PropTypes.oneOf('red', 'blue'); + + expect(console.error).toHaveBeenCalled(); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Invalid argument supplied to oneOf, expected an instance of array.', ); + + typeCheckPass(PropTypes.oneOf('red', 'blue'), 'red'); }); - it('should warn for invalid values', function() { + it('should warn for invalid values', () => { typeCheckFail( @@ -586,3 +814,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of value `true` supplied to ' + - '`testComponent`, expected one of ["red","blue"].' + '`testComponent`, expected one of ["red","blue"].', ); @@ -592,3 +820,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of value `` supplied to `testComponent`, ' + - 'expected one of ["red","blue"].' + 'expected one of ["red","blue"].', ); @@ -598,3 +826,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of value `` supplied to `testComponent`, ' + - 'expected one of ["red","blue"].' + 'expected one of ["red","blue"].', ); @@ -604,3 +832,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of value `false` supplied to ' + - '`testComponent`, expected one of [0,"false"].' + '`testComponent`, expected one of [0,"false"].', ); @@ -608,3 +836,3 @@ describe('ReactPropTypes', function() { - it('should not warn for valid values', function() { + it('should not warn for valid values', () => { typeCheckPass(PropTypes.oneOf(['red', 'blue']), 'red'); @@ -614,3 +842,3 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass(PropTypes.oneOf(['red', 'blue']), null); @@ -619,13 +847,11 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( - PropTypes.oneOf(['red', 'blue']).isRequired, - null, - requiredMessage - ); - typeCheckFail( - PropTypes.oneOf(['red', 'blue']).isRequired, - undefined, - requiredMessage - ); + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.oneOf(['red', 'blue']).isRequired); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.oneOf(['red', 'blue']), true); + expectWarningInDevelopment(PropTypes.oneOf(['red', 'blue']), null); + expectWarningInDevelopment(PropTypes.oneOf(['red', 'blue']), undefined); }); @@ -633,12 +859,17 @@ describe('ReactPropTypes', function() { - describe('Union Types', function() { - it('should fail for invalid argument', function() { - typeCheckFail( - PropTypes.oneOfType(PropTypes.string, PropTypes.number), - 'red', - 'Invalid argument supplied to oneOfType, expected an instance of array.' + describe('Union Types', () => { + it('should warn but not error for invalid argument', () => { + spyOn(console, 'error'); + + PropTypes.oneOfType(PropTypes.string, PropTypes.number); + + expect(console.error).toHaveBeenCalled(); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Invalid argument supplied to oneOfType, expected an instance of array.', ); + + typeCheckPass(PropTypes.oneOf(PropTypes.string, PropTypes.number), []); }); - it('should warn if none of the types are valid', function() { + it('should warn if none of the types are valid', () => { typeCheckFail( @@ -646,3 +877,3 @@ describe('ReactPropTypes', function() { [], - 'Invalid prop `testProp` supplied to `testComponent`.' + 'Invalid prop `testProp` supplied to `testComponent`.', ); @@ -656,3 +887,3 @@ describe('ReactPropTypes', function() { {c: 1}, - 'Invalid prop `testProp` supplied to `testComponent`.' + 'Invalid prop `testProp` supplied to `testComponent`.', ); @@ -660,7 +891,4 @@ describe('ReactPropTypes', function() { - it('should not warn if one of the types are valid', function() { - var checker = PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]); + it('should not warn if one of the types are valid', () => { + var checker = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); typeCheckPass(checker, null); @@ -677,8 +905,10 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), null + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + null, ); typeCheckPass( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), undefined + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + undefined, ); @@ -686,12 +916,21 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + [], + ); + expectWarningInDevelopment( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), null, - requiredMessage ); - typeCheckFail( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + expectWarningInDevelopment( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), undefined, - requiredMessage ); @@ -700,4 +939,4 @@ describe('ReactPropTypes', function() { - describe('Shape Types', function() { - it('should warn for non objects', function() { + describe('Shape Types', () => { + it('should warn for non objects', () => { typeCheckFail( @@ -706,3 +945,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `string` supplied to ' + - '`testComponent`, expected `object`.' + '`testComponent`, expected `object`.', ); @@ -712,3 +951,3 @@ describe('ReactPropTypes', function() { 'Invalid prop `testProp` of type `array` supplied to ' + - '`testComponent`, expected `object`.' + '`testComponent`, expected `object`.', ); @@ -716,3 +955,3 @@ describe('ReactPropTypes', function() { - it('should not warn for empty values', function() { + it('should not warn for empty values', () => { typeCheckPass(PropTypes.shape({}), undefined); @@ -722,3 +961,3 @@ describe('ReactPropTypes', function() { - it('should not warn for an empty object', function() { + it('should not warn for an empty object', () => { typeCheckPass(PropTypes.shape({}).isRequired, {}); @@ -726,3 +965,3 @@ describe('ReactPropTypes', function() { - it('should not warn for non specified types', function() { + it('should not warn for non specified types', () => { typeCheckPass(PropTypes.shape({}), {key: 1}); @@ -730,3 +969,3 @@ describe('ReactPropTypes', function() { - it('should not warn for valid types', function() { + it('should not warn for valid types', () => { typeCheckPass(PropTypes.shape({key: PropTypes.number}), {key: 1}); @@ -734,3 +973,3 @@ describe('ReactPropTypes', function() { - it('should warn for required valid types', function() { + it('should warn for required valid types', () => { typeCheckFail( @@ -738,3 +977,4 @@ describe('ReactPropTypes', function() { {}, - 'Required prop `testProp.key` was not specified in `testComponent`.' + 'The prop `testProp.key` is marked as required in `testComponent`, ' + + 'but its value is `undefined`.', ); @@ -742,3 +982,3 @@ describe('ReactPropTypes', function() { - it('should warn for the first required type', function() { + it('should warn for the first required type', () => { typeCheckFail( @@ -749,3 +989,4 @@ describe('ReactPropTypes', function() { {}, - 'Required prop `testProp.key` was not specified in `testComponent`.' + 'The prop `testProp.key` is marked as required in `testComponent`, ' + + 'but its value is `undefined`.', ); @@ -753,7 +994,8 @@ describe('ReactPropTypes', function() { - it('should warn for invalid key types', function() { - typeCheckFail(PropTypes.shape({key: PropTypes.number}), + it('should warn for invalid key types', () => { + typeCheckFail( + PropTypes.shape({key: PropTypes.number}), {key: 'abc'}, 'Invalid prop `testProp.key` of type `string` supplied to `testComponent`, ' + - 'expected `number`.' + 'expected `number`.', ); @@ -761,8 +1003,10 @@ describe('ReactPropTypes', function() { - it('should be implicitly optional and not warn without values', function() { + it('should be implicitly optional and not warn without values', () => { typeCheckPass( - PropTypes.shape(PropTypes.shape({key: PropTypes.number})), null + PropTypes.shape(PropTypes.shape({key: PropTypes.number})), + null, ); typeCheckPass( - PropTypes.shape(PropTypes.shape({key: PropTypes.number})), undefined + PropTypes.shape(PropTypes.shape({key: PropTypes.number})), + undefined, ); @@ -770,13 +1014,53 @@ describe('ReactPropTypes', function() { - it('should warn for missing required values', function() { - typeCheckFail( + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.shape({key: PropTypes.number}).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.shape({}), 'some string'); + expectWarningInDevelopment(PropTypes.shape({foo: PropTypes.number}), { + foo: 42, + }); + expectWarningInDevelopment( PropTypes.shape({key: PropTypes.number}).isRequired, null, - requiredMessage ); - typeCheckFail( + expectWarningInDevelopment( PropTypes.shape({key: PropTypes.number}).isRequired, undefined, - requiredMessage ); + expectWarningInDevelopment(PropTypes.element,
); + }); + }); + + describe('Symbol Type', () => { + it('should warn for non-symbol', () => { + typeCheckFail( + PropTypes.symbol, + 'hello', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected `symbol`.', + ); + typeCheckFail( + PropTypes.symbol, + function() {}, + 'Invalid prop `testProp` of type `function` supplied to ' + + '`testComponent`, expected `symbol`.', + ); + typeCheckFail( + PropTypes.symbol, + { + '@@toStringTag': 'Katana', + }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected `symbol`.', + ); + }); + + it('should not warn for a polyfilled Symbol', () => { + var CoreSymbol = require('core-js/library/es6/symbol'); + typeCheckPass(PropTypes.symbol, CoreSymbol('core-js')); }); @@ -784,4 +1068,4 @@ describe('ReactPropTypes', function() { - describe('Custom validator', function() { - beforeEach(function() { + describe('Custom validator', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -789,41 +1073,41 @@ describe('ReactPropTypes', function() { - it('should have been called with the right params', function() { + it('should have been called with the right params', () => { var spy = jasmine.createSpy(); - Component = React.createClass({ - propTypes: {num: spy}, + Component = class extends React.Component { + static propTypes = {num: spy}; - render: function() { + render() { return
; - }, - }); + } + }; var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); + ReactTestUtils.renderIntoDocument(instance); - expect(spy.argsForCall.length).toBe(2); // temp double validation - expect(spy.argsForCall[0][1]).toBe('num'); - expect(spy.argsForCall[0][2]).toBe('Component'); + expect(spy.calls.count()).toBe(1); + expect(spy.calls.argsFor(0)[1]).toBe('num'); }); - it('should have been called even if the prop is not present', function() { + it('should have been called even if the prop is not present', () => { var spy = jasmine.createSpy(); - Component = React.createClass({ - propTypes: {num: spy}, + Component = class extends React.Component { + static propTypes = {num: spy}; - render: function() { + render() { return
; - }, - }); + } + }; var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); + ReactTestUtils.renderIntoDocument(instance); - expect(spy.argsForCall.length).toBe(2); // temp double validation + expect(spy.calls.count()).toBe(1); + expect(spy.calls.argsFor(0)[1]).toBe('num'); }); - it('should have received the validator\'s return value', function() { + it("should have received the validator's return value", () => { spyOn(console, 'error'); - - var spy = jasmine.createSpy().andCallFake( - function(props, propName, componentName) { + var spy = jasmine + .createSpy() + .and.callFake(function(props, propName, componentName) { if (props[propName] !== 5) { @@ -831,17 +1115,19 @@ describe('ReactPropTypes', function() { } - } - ); - Component = React.createClass({ - propTypes: {num: spy}, + }); + Component = class extends React.Component { + static propTypes = {num: spy}; - render: function() { + render() { return
; - }, - }); + } + }; var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: num must be 5!' + ReactTestUtils.renderIntoDocument(instance); + expect(console.error.calls.count()).toBe(1); + expect( + console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)'), + ).toBe( + 'Warning: Failed prop type: num must be 5!\n' + + ' in Component (at **)', ); @@ -849,24 +1135,21 @@ describe('ReactPropTypes', function() { - it('should not warn if the validator returned null', - function() { - spyOn(console, 'error'); - - var spy = jasmine.createSpy().andCallFake( - function(props, propName, componentName) { - return null; - } - ); - Component = React.createClass({ - propTypes: {num: spy}, - - render: function() { - return
; - }, + it('should not warn if the validator returned null', () => { + spyOn(console, 'error'); + var spy = jasmine + .createSpy() + .and.callFake(function(props, propName, componentName) { + return null; }); + Component = class extends React.Component { + static propTypes = {num: spy}; - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(console.error.argsForCall.length).toBe(0); - } - ); + render() { + return
; + } + }; + + var instance = ; + ReactTestUtils.renderIntoDocument(instance); + expect(console.error.calls.count()).toBe(0); + }); }); diff --git a/src/isomorphic/deprecated/OrderedMap.js b/src/isomorphic/deprecated/OrderedMap.js deleted file mode 100644 index 4336cdc38..000000000 --- a/src/isomorphic/deprecated/OrderedMap.js +++ /dev/null @@ -1,505 +0,0 @@ -/** - * Copyright 2013-present, 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. - * - * @providesModule OrderedMap - */ - -'use strict'; - -var invariant = require('invariant'); - -var PREFIX = 'key:'; - -/** - * Utility to extract a backing object from an initialization `Array`, allowing - * the caller to assist in resolving the unique ID for each entry via the - * `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or - * numbers. - * @param {Array} arr Array of items. - * @param {function} keyExtractor Extracts a unique key from each item. - * @return {Object} Map from unique key to originating value that the key was - * extracted from. - * @throws Exception if the initialization array has duplicate extracted keys. - */ -function extractObjectFromArray(arr, keyExtractor) { - var normalizedObj = {}; - for (var i = 0; i < arr.length; i++) { - var item = arr[i]; - var key = keyExtractor(item); - assertValidPublicKey(key); - var normalizedKey = PREFIX + key; - invariant( - !(normalizedKey in normalizedObj), - 'OrderedMap: IDs returned by the key extraction function must be unique.' - ); - normalizedObj[normalizedKey] = item; - } - return normalizedObj; -} - -/** - * Utility class for mappings with ordering. This class is to be used in an - * immutable manner. A `OrderedMap` is very much like the native JavaScript - * object, where keys map to values via the `get()` function. Also, like the - * native JavaScript object, there is an ordering associated with the mapping. - * This class is helpful because it eliminates many of the pitfalls that come - * with the native JavaScript ordered mappings. Specifically, there are - * inconsistencies with numeric keys in some JavaScript implementations - * (enumeration ordering). This class protects against those pitfalls and - * provides functional utilities for dealing with these `OrderedMap`s. - * - * - TODO: - * - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s. - * - mapReverse(). - * - * @class {OrderedMap} - * @constructor {OrderedMap} - * @param {Object} normalizedObj Object that is known to be a defensive copy of - * caller supplied data. We require a defensive copy to guard against callers - * mutating. It is also assumed that the keys of `normalizedObj` have been - * normalized and do not contain any numeric-appearing strings. - * @param {number} computedLength The precomputed length of `_normalizedObj` - * keys. - * @private - */ -function OrderedMapImpl(normalizedObj, computedLength) { - this._normalizedObj = normalizedObj; - this._computedPositions = null; - this.length = computedLength; -} - -/** - * Validates a "public" key - that is, one that the public facing API supplies. - * The key is then normalized for internal storage. In order to be considered - * valid, all keys must be non-empty, defined, non-null strings or numbers. - * - * @param {string?} key Validates that the key is suitable for use in a - * `OrderedMap`. - * @throws Error if key is not appropriate for use in `OrderedMap`. - */ -function assertValidPublicKey(key) { - invariant( - key !== '' && (typeof key === 'string' || typeof key === 'number'), - 'OrderedMap: Key must be non-empty, non-null string or number.' - ); -} - -/** - * Validates that arguments to range operations are within the correct limits. - * - * @param {number} start Start of range. - * @param {number} length Length of range. - * @param {number} actualLen Actual length of range that should not be - * exceeded. - * @throws Error if range arguments are out of bounds. - */ -function assertValidRangeIndices(start, length, actualLen) { - invariant( - typeof start === 'number' && - typeof length === 'number' && - length >= 0 && - start >= 0 && - start + length <= actualLen, - 'OrderedMap: `mapRange` and `forEachRange` expect non-negative start and ' + - 'length arguments within the bounds of the instance.' - ); -} - -/** - * Merges two "normalized" objects (objects who's key have been normalized) into - * a `OrderedMap`. - * - * @param {Object} a Object of key value pairs. - * @param {Object} b Object of key value pairs. - * @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`. - */ -function _fromNormalizedObjects(a, b) { - // Second optional, both must be plain JavaScript objects. - invariant( - a && a.constructor === Object && (!b || b.constructor === Object), - 'OrderedMap: Corrupted instance of OrderedMap detected.' - ); - - var newSet = {}; - var length = 0; - var key; - for (key in a) { - if (a.hasOwnProperty(key)) { - newSet[key] = a[key]; - length++; - } - } - - for (key in b) { - if (b.hasOwnProperty(key)) { - // Increment length if not already added via first object (a) - if (!(key in newSet)) { - length++; - } - newSet[key] = b[key]; - } - } - return new OrderedMapImpl(newSet, length); -} - -/** - * Methods for `OrderedMap` instances. - * - * @lends OrderedMap.prototype - * TODO: Make this data structure lazy, unify with LazyArray. - * TODO: Unify this with ImmutableObject - it is to be used immutably. - * TODO: If so, consider providing `fromObject` API. - * TODO: Create faster implementation of merging/mapping from original Array, - * without having to first create an object - simply for the sake of merging. - */ -var OrderedMapMethods = { - - /** - * Returns whether or not a given key is present in the map. - * - * @param {string} key Valid string key to lookup membership for. - * @return {boolean} Whether or not `key` is a member of the map. - * @throws Error if provided known invalid key. - */ - has: function(key) { - assertValidPublicKey(key); - var normalizedKey = PREFIX + key; - return normalizedKey in this._normalizedObj; - }, - - /** - * Returns the object for a given key, or `undefined` if not present. To - * distinguish an undefined entry vs not being in the set, use `has()`. - * - * @param {string} key String key to lookup the value for. - * @return {Object?} Object at key `key`, or undefined if not in map. - * @throws Error if provided known invalid key. - */ - get: function(key) { - assertValidPublicKey(key); - var normalizedKey = PREFIX + key; - return this.has(key) ? this._normalizedObj[normalizedKey] : undefined; - }, - - /** - * Merges, appending new keys to the end of the ordering. Keys in `orderedMap` - * that are redundant with `this`, maintain the same ordering index that they - * had in `this`. This is how standard JavaScript object merging would work. - * If you wish to prepend a `OrderedMap` to the beginning of another - * `OrderedMap` then simply reverse the order of operation. This is the analog - * to `merge(x, y)`. - * - * @param {OrderedMap} orderedMap OrderedMap to merge onto the end. - * @return {OrderedMap} New OrderedMap that represents the result of the - * merge. - */ - merge: function(orderedMap) { - invariant( - orderedMap instanceof OrderedMapImpl, - 'OrderedMap.merge(...): Expected an OrderedMap instance.' - ); - return _fromNormalizedObjects( - this._normalizedObj, - orderedMap._normalizedObj - ); - }, - - /** - * Functional map API. Returns a new `OrderedMap`. - * - * @param {Function} cb Callback to invoke for each item. - * @param {Object?=} context Context to invoke callback from. - * @return {OrderedMap} OrderedMap that results from mapping. - */ - map: function(cb, context) { - return this.mapRange(cb, 0, this.length, context); - }, - - /** - * The callback `cb` is invoked with the arguments (item, key, - * indexInOriginal). - * - * @param {Function} cb Determines result for each item. - * @param {number} start Start index of map range. - * @param {end} length End index of map range. - * @param {*!?} context Context of callback invocation. - * @return {OrderedMap} OrderedMap resulting from mapping the range. - */ - mapRange: function(cb, start, length, context) { - var thisSet = this._normalizedObj; - var newSet = {}; - var i = 0; - assertValidRangeIndices(start, length, this.length); - var end = start + length - 1; - for (var key in thisSet) { - if (thisSet.hasOwnProperty(key)) { - if (i >= start) { - if (i > end) { - break; - } - var item = thisSet[key]; - newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i); - } - i++; - } - } - return new OrderedMapImpl(newSet, length); - }, - - /** - * Function filter API. Returns new `OrderedMap`. - * - * @param {Function} cb Callback to invoke for each item. - * @param {Object?=} context Context to invoke callback from. - * @return {OrderedMap} OrderedMap that results from filtering. - */ - filter: function(cb, context) { - return this.filterRange(cb, 0, this.length, context); - }, - - /** - * The callback `cb` is invoked with the arguments (item, key, - * indexInOriginal). - * - * @param {Function} cb Returns true if item should be in result. - * @param {number} start Start index of filter range. - * @param {number} length End index of map range. - * @param {*!?} context Context of callback invocation. - * @return {OrderedMap} OrderedMap resulting from filtering the range. - */ - filterRange: function(cb, start, length, context) { - var newSet = {}; - var newSetLength = 0; - this.forEachRange(function(item, key, originalIndex) { - if (cb.call(context, item, key, originalIndex)) { - var normalizedKey = PREFIX + key; - newSet[normalizedKey] = item; - newSetLength++; - } - }, start, length); - return new OrderedMapImpl(newSet, newSetLength); - }, - - forEach: function(cb, context) { - this.forEachRange(cb, 0, this.length, context); - }, - - forEachRange: function(cb, start, length, context) { - assertValidRangeIndices(start, length, this.length); - var thisSet = this._normalizedObj; - var i = 0; - var end = start + length - 1; - for (var key in thisSet) { - if (thisSet.hasOwnProperty(key)) { - if (i >= start) { - if (i > end) { - break; - } - var item = thisSet[key]; - cb.call(context, item, key.substr(PREFIX.length), i); - } - i++; - } - } - }, - - /** - * Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll - * impose an additional restriction here that the length of mapping be greater - * than zero - the only reason is that there are many ways to express length - * zero in terms of two keys and that is confusing. - */ - mapKeyRange: function(cb, startKey, endKey, context) { - var startIndex = this.indexOfKey(startKey); - var endIndex = this.indexOfKey(endKey); - invariant( - startIndex !== undefined && endIndex !== undefined, - 'mapKeyRange must be given keys that are present.' - ); - invariant( - endIndex >= startIndex, - 'OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.' - ); - return this.mapRange(cb, startIndex, (endIndex - startIndex) + 1, context); - }, - - forEachKeyRange: function(cb, startKey, endKey, context) { - var startIndex = this.indexOfKey(startKey); - var endIndex = this.indexOfKey(endKey); - invariant( - startIndex !== undefined && endIndex !== undefined, - 'forEachKeyRange must be given keys that are present.' - ); - invariant( - endIndex >= startIndex, - 'OrderedMap.forEachKeyRange(...): `endKey` must not come before ' + - '`startIndex`.' - ); - this.forEachRange(cb, startIndex, (endIndex - startIndex) + 1, context); - }, - - /** - * @param {number} pos Index to search for key at. - * @return {string|undefined} Either the key at index `pos` or undefined if - * not in map. - */ - keyAtIndex: function(pos) { - var computedPositions = this._getOrComputePositions(); - var keyAtPos = computedPositions.keyByIndex[pos]; - return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined; - }, - - /** - * @param {string} key String key from which to find the next key. - * @return {string|undefined} Either the next key, or undefined if there is no - * next key. - * @throws Error if `key` is not in this `OrderedMap`. - */ - keyAfter: function(key) { - return this.nthKeyAfter(key, 1); - }, - - /** - * @param {string} key String key from which to find the preceding key. - * @return {string|undefined} Either the preceding key, or undefined if there - * is no preceding.key. - * @throws Error if `key` is not in this `OrderedMap`. - */ - keyBefore: function(key) { - return this.nthKeyBefore(key, 1); - }, - - /** - * @param {string} key String key from which to find a following key. - * @param {number} n Distance to scan forward after `key`. - * @return {string|undefined} Either the nth key after `key`, or undefined if - * there is no next key. - * @throws Error if `key` is not in this `OrderedMap`. - */ - nthKeyAfter: function(key, n) { - var curIndex = this.indexOfKey(key); - invariant( - curIndex !== undefined, - 'OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.', - key - ); - return this.keyAtIndex(curIndex + n); - }, - - /** - * @param {string} key String key from which to find a preceding key. - * @param {number} n Distance to scan backwards before `key`. - * @return {string|undefined} Either the nth key before `key`, or undefined if - * there is no previous key. - * @throws Error if `key` is not in this `OrderedMap`. - */ - nthKeyBefore: function(key, n) { - return this.nthKeyAfter(key, -n); - }, - - /** - * @param {string} key Key to find the index of. - * @return {number|undefined} Index of the provided key, or `undefined` if the - * key is not found. - */ - indexOfKey: function(key) { - assertValidPublicKey(key); - var normalizedKey = PREFIX + key; - var computedPositions = this._getOrComputePositions(); - var computedPosition = computedPositions.indexByKey[normalizedKey]; - // Just writing it this way to make it clear this is intentional. - return computedPosition === undefined ? undefined : computedPosition; - }, - - /** - * @return {Array} An ordered array of this object's values. - */ - toArray: function() { - var result = []; - var thisSet = this._normalizedObj; - for (var key in thisSet) { - if (thisSet.hasOwnProperty(key)) { - result.push(thisSet[key]); - } - } - return result; - }, - - /** - * Finds the key at a given position, or indicates via `undefined` that that - * position does not exist in the `OrderedMap`. It is appropriate to return - * undefined, indicating that the key doesn't exist in the `OrderedMap` - * because `undefined` is not ever a valid `OrderedMap` key. - * - * @private - * @return {string?} Name of the item at position `pos`, or `undefined` if - * there is no item at that position. - */ - _getOrComputePositions: function() { - // TODO: Entertain computing this at construction time in some less - // performance critical paths. - var computedPositions = this._computedPositions; - if (!computedPositions) { - this._computePositions(); - } - return this._computedPositions; - }, - - /** - * Precomputes the index/key mapping for future lookup. Since `OrderedMap`s - * are immutable, there is only ever a need to perform this once. - * @private - */ - _computePositions: function() { - this._computedPositions = { - keyByIndex: {}, - indexByKey: {}, - }; - var keyByIndex = this._computedPositions.keyByIndex; - var indexByKey = this._computedPositions.indexByKey; - var index = 0; - var thisSet = this._normalizedObj; - for (var key in thisSet) { - if (thisSet.hasOwnProperty(key)) { - keyByIndex[index] = key; - indexByKey[key] = index; - index++; - } - } - }, -}; - -Object.assign(OrderedMapImpl.prototype, OrderedMapMethods); - -var OrderedMap = { - from: function(orderedMap) { - invariant( - orderedMap instanceof OrderedMapImpl, - 'OrderedMap.from(...): Expected an OrderedMap instance.' - ); - return _fromNormalizedObjects(orderedMap._normalizedObj, null); - }, - - fromArray: function(arr, keyExtractor) { - invariant( - Array.isArray(arr), - 'OrderedMap.fromArray(...): First argument must be an array.' - ); - invariant( - typeof keyExtractor === 'function', - 'OrderedMap.fromArray(...): Second argument must be a function used ' + - 'to determine the unique key for each entry.' - ); - return new OrderedMapImpl( - extractObjectFromArray(arr, keyExtractor), - arr.length - ); - }, -}; - -module.exports = OrderedMap; diff --git a/src/isomorphic/deprecated/ReactPropTransferer.js b/src/isomorphic/deprecated/ReactPropTransferer.js deleted file mode 100644 index da009cafe..000000000 --- a/src/isomorphic/deprecated/ReactPropTransferer.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactPropTransferer - */ - -'use strict'; - -var emptyFunction = require('emptyFunction'); -var joinClasses = require('joinClasses'); - -/** - * Creates a transfer strategy that will merge prop values using the supplied - * `mergeStrategy`. If a prop was previously unset, this just sets it. - * - * @param {function} mergeStrategy - * @return {function} - */ -function createTransferStrategy(mergeStrategy) { - return function(props, key, value) { - if (!props.hasOwnProperty(key)) { - props[key] = value; - } else { - props[key] = mergeStrategy(props[key], value); - } - }; -} - -var transferStrategyMerge = createTransferStrategy(function(a, b) { - // `merge` overrides the first object's (`props[key]` above) keys using the - // second object's (`value`) keys. An object's style's existing `propA` would - // get overridden. Flip the order here. - return Object.assign({}, b, a); -}); - -/** - * Transfer strategies dictate how props are transferred by `transferPropsTo`. - * NOTE: if you add any more exceptions to this list you should be sure to - * update `cloneWithProps()` accordingly. - */ -var TransferStrategies = { - /** - * Never transfer `children`. - */ - children: emptyFunction, - /** - * Transfer the `className` prop by merging them. - */ - className: createTransferStrategy(joinClasses), - /** - * Transfer the `style` prop (which is an object) by merging them. - */ - style: transferStrategyMerge, -}; - -/** - * Mutates the first argument by transferring the properties from the second - * argument. - * - * @param {object} props - * @param {object} newProps - * @return {object} - */ -function transferInto(props, newProps) { - for (var thisKey in newProps) { - if (!newProps.hasOwnProperty(thisKey)) { - continue; - } - - var transferStrategy = TransferStrategies[thisKey]; - - if (transferStrategy && TransferStrategies.hasOwnProperty(thisKey)) { - transferStrategy(props, thisKey, newProps[thisKey]); - } else if (!props.hasOwnProperty(thisKey)) { - props[thisKey] = newProps[thisKey]; - } - } - return props; -} - -/** - * ReactPropTransferer are capable of transferring props to another component - * using a `transferPropsTo` method. - * - * @class ReactPropTransferer - */ -var ReactPropTransferer = { - - /** - * Merge two props objects using TransferStrategies. - * - * @param {object} oldProps original props (they take precedence) - * @param {object} newProps new props to merge in - * @return {object} a new object containing both sets of props merged. - */ - mergeProps: function(oldProps, newProps) { - return transferInto(Object.assign({}, oldProps), newProps); - }, - -}; - -module.exports = ReactPropTransferer; diff --git a/src/isomorphic/getNextDebugID.js b/src/isomorphic/getNextDebugID.js new file mode 100644 index 000000000..6e7ecb43b --- /dev/null +++ b/src/isomorphic/getNextDebugID.js @@ -0,0 +1,21 @@ +/** + * Copyright 2013-present, 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. + * + * @providesModule getNextDebugID + * @flow + */ + +'use strict'; + +var nextDebugID = 1; + +function getNextDebugID(): number { + return nextDebugID++; +} + +module.exports = getNextDebugID; diff --git a/src/isomorphic/ReactInstrumentation.js b/src/isomorphic/hooks/ReactComponentTreeDevtool.js similarity index 68% rename from src/isomorphic/ReactInstrumentation.js rename to src/isomorphic/hooks/ReactComponentTreeDevtool.js index 066da6aaa..8e9898e3c 100644 --- a/src/isomorphic/ReactInstrumentation.js +++ b/src/isomorphic/hooks/ReactComponentTreeDevtool.js @@ -8,9 +8,7 @@ * - * @providesModule ReactInstrumentation + * @providesModule ReactComponentTreeDevtool */ - 'use strict'; -var ReactDebugTool = require('ReactDebugTool'); - -module.exports = {debugTool: ReactDebugTool}; +// TODO remove this proxy when RN/www gets updated +module.exports = require('ReactComponentTreeHook'); diff --git a/src/isomorphic/hooks/ReactComponentTreeHook.js b/src/isomorphic/hooks/ReactComponentTreeHook.js new file mode 100644 index 000000000..6ff41657a --- /dev/null +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -0,0 +1,458 @@ +/** + * Copyright 2016-present, 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. + * + * @flow + * @providesModule ReactComponentTreeHook + */ + +'use strict'; + +var ReactCurrentOwner = require('ReactCurrentOwner'); + +var invariant = require('invariant'); +var warning = require('warning'); + +import type {ReactElement, Source} from 'ReactElementType'; +import type {DebugID} from 'ReactInstanceType'; + +function isNative(fn) { + // Based on isNative() from Lodash + var funcToString = Function.prototype.toString; + var hasOwnProperty = Object.prototype.hasOwnProperty; + var reIsNative = RegExp( + '^' + + funcToString + // Take an example native function source for comparison + .call(hasOwnProperty) + // Strip regex characters so we can use it for regex + .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') + // Remove hasOwnProperty from the template to make it generic + .replace( + /hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, + '$1.*?', + ) + + '$', + ); + try { + var source = funcToString.call(fn); + return reIsNative.test(source); + } catch (err) { + return false; + } +} + +var canUseCollections = + // Array.from + typeof Array.from === 'function' && + // Map + typeof Map === 'function' && + isNative(Map) && + // Map.prototype.keys + Map.prototype != null && + typeof Map.prototype.keys === 'function' && + isNative(Map.prototype.keys) && + // Set + typeof Set === 'function' && + isNative(Set) && + // Set.prototype.keys + Set.prototype != null && + typeof Set.prototype.keys === 'function' && + isNative(Set.prototype.keys); + +var setItem; +var getItem; +var removeItem; +var getItemIDs; +var addRoot; +var removeRoot; +var getRootIDs; + +if (canUseCollections) { + var itemMap = new Map(); + var rootIDSet = new Set(); + + setItem = function(id, item) { + itemMap.set(id, item); + }; + getItem = function(id) { + return itemMap.get(id); + }; + removeItem = function(id) { + itemMap.delete(id); + }; + getItemIDs = function() { + return Array.from(itemMap.keys()); + }; + + addRoot = function(id) { + rootIDSet.add(id); + }; + removeRoot = function(id) { + rootIDSet.delete(id); + }; + getRootIDs = function() { + return Array.from(rootIDSet.keys()); + }; +} else { + var itemByKey = {}; + var rootByKey = {}; + + // Use non-numeric keys to prevent V8 performance issues: + // https://github.com/facebook/react/pull/7232 + var getKeyFromID = function(id: DebugID): string { + return '.' + id; + }; + var getIDFromKey = function(key: string): DebugID { + return parseInt(key.substr(1), 10); + }; + + setItem = function(id, item) { + var key = getKeyFromID(id); + itemByKey[key] = item; + }; + getItem = function(id) { + var key = getKeyFromID(id); + return itemByKey[key]; + }; + removeItem = function(id) { + var key = getKeyFromID(id); + delete itemByKey[key]; + }; + getItemIDs = function() { + return Object.keys(itemByKey).map(getIDFromKey); + }; + + addRoot = function(id) { + var key = getKeyFromID(id); + rootByKey[key] = true; + }; + removeRoot = function(id) { + var key = getKeyFromID(id); + delete rootByKey[key]; + }; + getRootIDs = function() { + return Object.keys(rootByKey).map(getIDFromKey); + }; +} + +var unmountedIDs: Array = []; + +function purgeDeep(id) { + var item = getItem(id); + if (item) { + var {childIDs} = item; + removeItem(id); + childIDs.forEach(purgeDeep); + } +} + +function describeComponentFrame(name, source, ownerName) { + return ( + '\n in ' + + (name || 'Unknown') + + (source + ? ' (at ' + + source.fileName.replace(/^.*[\\\/]/, '') + + ':' + + source.lineNumber + + ')' + : ownerName ? ' (created by ' + ownerName + ')' : '') + ); +} + +function getDisplayName(element: ?ReactElement): string { + if (element == null) { + return '#empty'; + } else if (typeof element === 'string' || typeof element === 'number') { + return '#text'; + } else if (typeof element.type === 'string') { + return element.type; + } else { + return element.type.displayName || element.type.name || 'Unknown'; + } +} + +function describeID(id: DebugID): string { + var name = ReactComponentTreeHook.getDisplayName(id); + var element = ReactComponentTreeHook.getElement(id); + var ownerID = ReactComponentTreeHook.getOwnerID(id); + var ownerName; + if (ownerID) { + ownerName = ReactComponentTreeHook.getDisplayName(ownerID); + } + warning( + element, + 'ReactComponentTreeHook: Missing React element for debugID %s when ' + + 'building stack', + id, + ); + return describeComponentFrame(name, element && element._source, ownerName); +} + +var ReactComponentTreeHook = { + onSetChildren(id: DebugID, nextChildIDs: Array): void { + var item = getItem(id); + invariant(item, 'Item must have been set'); + item.childIDs = nextChildIDs; + + for (var i = 0; i < nextChildIDs.length; i++) { + var nextChildID = nextChildIDs[i]; + var nextChild = getItem(nextChildID); + invariant( + nextChild, + 'Expected hook events to fire for the child ' + + 'before its parent includes it in onSetChildren().', + ); + invariant( + nextChild.childIDs != null || + typeof nextChild.element !== 'object' || + nextChild.element == null, + 'Expected onSetChildren() to fire for a container child ' + + 'before its parent includes it in onSetChildren().', + ); + invariant( + nextChild.isMounted, + 'Expected onMountComponent() to fire for the child ' + + 'before its parent includes it in onSetChildren().', + ); + if (nextChild.parentID == null) { + nextChild.parentID = id; + // TODO: This shouldn't be necessary but mounting a new root during in + // componentWillMount currently causes not-yet-mounted components to + // be purged from our tree data so their parent id is missing. + } + invariant( + nextChild.parentID === id, + 'Expected onBeforeMountComponent() parent and onSetChildren() to ' + + 'be consistent (%s has parents %s and %s).', + nextChildID, + nextChild.parentID, + id, + ); + } + }, + + onBeforeMountComponent( + id: DebugID, + element: ReactElement, + parentID: DebugID, + ): void { + var item = { + element, + parentID, + text: null, + childIDs: [], + isMounted: false, + updateCount: 0, + }; + setItem(id, item); + }, + + onBeforeUpdateComponent(id: DebugID, element: ReactElement): void { + var item = getItem(id); + if (!item || !item.isMounted) { + // We may end up here as a result of setState() in componentWillUnmount(). + // In this case, ignore the element. + return; + } + item.element = element; + }, + + onMountComponent(id: DebugID): void { + var item = getItem(id); + invariant(item, 'Item must have been set'); + item.isMounted = true; + var isRoot = item.parentID === 0; + if (isRoot) { + addRoot(id); + } + }, + + onUpdateComponent(id: DebugID): void { + var item = getItem(id); + if (!item || !item.isMounted) { + // We may end up here as a result of setState() in componentWillUnmount(). + // In this case, ignore the element. + return; + } + item.updateCount++; + }, + + onUnmountComponent(id: DebugID): void { + var item = getItem(id); + if (item) { + // We need to check if it exists. + // `item` might not exist if it is inside an error boundary, and a sibling + // error boundary child threw while mounting. Then this instance never + // got a chance to mount, but it still gets an unmounting event during + // the error boundary cleanup. + item.isMounted = false; + var isRoot = item.parentID === 0; + if (isRoot) { + removeRoot(id); + } + } + unmountedIDs.push(id); + }, + + purgeUnmountedComponents(): void { + if (ReactComponentTreeHook._preventPurging) { + // Should only be used for testing. + return; + } + + for (var i = 0; i < unmountedIDs.length; i++) { + var id = unmountedIDs[i]; + purgeDeep(id); + } + unmountedIDs.length = 0; + }, + + isMounted(id: DebugID): boolean { + var item = getItem(id); + return item ? item.isMounted : false; + }, + + getCurrentStackAddendum(topElement: ?ReactElement): string { + var info = ''; + if (topElement) { + var name = getDisplayName(topElement); + var owner = topElement._owner; + info += describeComponentFrame( + name, + topElement._source, + owner && owner.getName(), + ); + } + + var currentOwner = ReactCurrentOwner.current; + var id = currentOwner && currentOwner._debugID; + + info += ReactComponentTreeHook.getStackAddendumByID(id); + return info; + }, + + getStackAddendumByID(id: ?DebugID): string { + var info = ''; + while (id) { + info += describeID(id); + id = ReactComponentTreeHook.getParentID(id); + } + return info; + }, + + getChildIDs(id: DebugID): Array { + var item = getItem(id); + return item ? item.childIDs : []; + }, + + getDisplayName(id: DebugID): ?string { + var element = ReactComponentTreeHook.getElement(id); + if (!element) { + return null; + } + return getDisplayName(element); + }, + + getElement(id: DebugID): ?ReactElement { + var item = getItem(id); + return item ? item.element : null; + }, + + getOwnerID(id: DebugID): ?DebugID { + var element = ReactComponentTreeHook.getElement(id); + if (!element || !element._owner) { + return null; + } + return element._owner._debugID; + }, + + getParentID(id: DebugID): ?DebugID { + var item = getItem(id); + return item ? item.parentID : null; + }, + + getSource(id: DebugID): ?Source { + var item = getItem(id); + var element = item ? item.element : null; + var source = element != null ? element._source : null; + return source; + }, + + getText(id: DebugID): ?string { + var element = ReactComponentTreeHook.getElement(id); + if (typeof element === 'string') { + return element; + } else if (typeof element === 'number') { + return '' + element; + } else { + return null; + } + }, + + getUpdateCount(id: DebugID): number { + var item = getItem(id); + return item ? item.updateCount : 0; + }, + + getRootIDs, + getRegisteredIDs: getItemIDs, + + pushNonStandardWarningStack( + isCreatingElement: boolean, + currentSource: ?Source, + ) { + if (typeof console.reactStack !== 'function') { + return; + } + + var stack = []; + var currentOwner = ReactCurrentOwner.current; + var id = currentOwner && currentOwner._debugID; + + try { + if (isCreatingElement) { + stack.push({ + name: id ? ReactComponentTreeHook.getDisplayName(id) : null, + fileName: currentSource ? currentSource.fileName : null, + lineNumber: currentSource ? currentSource.lineNumber : null, + }); + } + + while (id) { + var element = ReactComponentTreeHook.getElement(id); + var parentID = ReactComponentTreeHook.getParentID(id); + var ownerID = ReactComponentTreeHook.getOwnerID(id); + var ownerName = ownerID + ? ReactComponentTreeHook.getDisplayName(ownerID) + : null; + var source = element && element._source; + stack.push({ + name: ownerName, + fileName: source ? source.fileName : null, + lineNumber: source ? source.lineNumber : null, + }); + id = parentID; + } + } catch (err) { + // Internal state is messed up. + // Stop building the stack (it's just a nice to have). + } + + console.reactStack(stack); + }, + + popNonStandardWarningStack() { + if (typeof console.reactStackEnd !== 'function') { + return; + } + console.reactStackEnd(); + }, +}; + +module.exports = ReactComponentTreeHook; diff --git a/src/isomorphic/modern/class/PropTypes.d.ts b/src/isomorphic/modern/class/PropTypes.d.ts new file mode 100644 index 000000000..a8802e66f --- /dev/null +++ b/src/isomorphic/modern/class/PropTypes.d.ts @@ -0,0 +1,19 @@ +/*! + * Copyright 2015-present, 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. + */ + +/** + * TypeScript Definition File for React. + * + * Full type definitions are not yet officially supported. These are mostly + * just helpers for the unit test. + */ + +declare module 'prop-types' { + export var string : any; +} diff --git a/src/isomorphic/modern/class/ReactComponent.js b/src/isomorphic/modern/class/ReactBaseClasses.js similarity index 76% rename from src/isomorphic/modern/class/ReactComponent.js rename to src/isomorphic/modern/class/ReactBaseClasses.js index a775abd67..ed8d31e9b 100644 --- a/src/isomorphic/modern/class/ReactComponent.js +++ b/src/isomorphic/modern/class/ReactBaseClasses.js @@ -8,3 +8,3 @@ * - * @providesModule ReactComponent + * @providesModule ReactBaseClasses */ @@ -14,3 +14,2 @@ var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue'); -var ReactInstrumentation = require('ReactInstrumentation'); @@ -19,3 +18,3 @@ var emptyObject = require('emptyObject'); var invariant = require('invariant'); -var warning = require('warning'); +var lowPriorityWarning = require('lowPriorityWarning'); @@ -63,15 +62,7 @@ ReactComponent.prototype.setState = function(partialState, callback) { typeof partialState === 'object' || - typeof partialState === 'function' || - partialState == null, + typeof partialState === 'function' || + partialState == null, 'setState(...): takes an object of state variables to update or a ' + - 'function which returns an object of state variables.' + 'function which returns an object of state variables.', ); - if (__DEV__) { - ReactInstrumentation.debugTool.onSetState(); - warning( - partialState != null, - 'setState(...): You passed an undefined or null state object; ' + - 'instead, use forceUpdate().' - ); - } this.updater.enqueueSetState(this, partialState); @@ -113,3 +104,3 @@ if (__DEV__) { 'Instead, make sure to clean up subscriptions and pending requests in ' + - 'componentWillUnmount to prevent memory leaks.', + 'componentWillUnmount to prevent memory leaks.', ], @@ -118,3 +109,3 @@ if (__DEV__) { 'Refactor your code to use setState instead (see ' + - 'https://github.com/facebook/react/issues/3236).', + 'https://github.com/facebook/react/issues/3236).', ], @@ -125,3 +116,3 @@ if (__DEV__) { get: function() { - warning( + lowPriorityWarning( false, @@ -129,3 +120,3 @@ if (__DEV__) { info[0], - info[1] + info[1], ); @@ -143,2 +134,26 @@ if (__DEV__) { -module.exports = ReactComponent; +/** + * Base class helpers for the updating state of a component. + */ +function ReactPureComponent(props, context, updater) { + // Duplicated from ReactComponent. + this.props = props; + this.context = context; + this.refs = emptyObject; + // We initialize the default updater but the real one gets injected by the + // renderer. + this.updater = updater || ReactNoopUpdateQueue; +} + +function ComponentDummy() {} +ComponentDummy.prototype = ReactComponent.prototype; +ReactPureComponent.prototype = new ComponentDummy(); +ReactPureComponent.prototype.constructor = ReactPureComponent; +// Avoid an extra prototype jump for these methods. +Object.assign(ReactPureComponent.prototype, ReactComponent.prototype); +ReactPureComponent.prototype.isPureReactComponent = true; + +module.exports = { + Component: ReactComponent, + PureComponent: ReactPureComponent, +}; diff --git a/src/isomorphic/modern/class/ReactNoopUpdateQueue.js b/src/isomorphic/modern/class/ReactNoopUpdateQueue.js index 2f0be661c..c9e03b723 100644 --- a/src/isomorphic/modern/class/ReactNoopUpdateQueue.js +++ b/src/isomorphic/modern/class/ReactNoopUpdateQueue.js @@ -15,4 +15,5 @@ var warning = require('warning'); -function warnTDZ(publicInstance, callerName) { +function warnNoop(publicInstance, callerName) { if (__DEV__) { + var constructor = publicInstance.constructor; warning( @@ -20,7 +21,8 @@ function warnTDZ(publicInstance, callerName) { '%s(...): Can only update a mounted or mounting component. ' + - 'This usually means you called %s() on an unmounted component. ' + - 'This is a no-op. Please check the code for the %s component.', + 'This usually means you called %s() on an unmounted component. ' + + 'This is a no-op. Please check the code for the %s component.', callerName, callerName, - publicInstance.constructor && publicInstance.constructor.displayName || '' + (constructor && (constructor.displayName || constructor.name)) || + 'ReactClass', ); @@ -33,3 +35,2 @@ function warnTDZ(publicInstance, callerName) { var ReactNoopUpdateQueue = { - /** @@ -53,3 +54,3 @@ var ReactNoopUpdateQueue = { */ - enqueueCallback: function(publicInstance, callback) { }, + enqueueCallback: function(publicInstance, callback) {}, @@ -69,3 +70,3 @@ var ReactNoopUpdateQueue = { enqueueForceUpdate: function(publicInstance) { - warnTDZ(publicInstance, 'forceUpdate'); + warnNoop(publicInstance, 'forceUpdate'); }, @@ -84,3 +85,3 @@ var ReactNoopUpdateQueue = { enqueueReplaceState: function(publicInstance, completeState) { - warnTDZ(publicInstance, 'replaceState'); + warnNoop(publicInstance, 'replaceState'); }, @@ -98,3 +99,3 @@ var ReactNoopUpdateQueue = { enqueueSetState: function(publicInstance, partialState) { - warnTDZ(publicInstance, 'setState'); + warnNoop(publicInstance, 'setState'); }, diff --git a/src/isomorphic/modern/class/__tests__/ReactClassEquivalence-test.js b/src/isomorphic/modern/class/__tests__/ReactClassEquivalence-test.js index 36cf5c75c..bfa6c3b21 100644 --- a/src/isomorphic/modern/class/__tests__/ReactClassEquivalence-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactClassEquivalence-test.js @@ -13,22 +13,62 @@ -var MetaMatchers = require('MetaMatchers'); +var spawnSync = require('child_process').spawnSync; +var path = require('path'); -describe('ReactClassEquivalence', function() { +describe('ReactClassEquivalence', () => { + it('tests the same thing for es6 classes and CoffeeScript', () => { + var result1 = runJest('ReactCoffeeScriptClass-test.coffee'); + var result2 = runJest('ReactES6Class-test.js'); + compareResults(result1, result2); + }); - beforeEach(function() { - this.addMatchers(MetaMatchers); + it('tests the same thing for es6 classes and TypeScript', () => { + var result1 = runJest('ReactTypeScriptClass-test.ts'); + var result2 = runJest('ReactES6Class-test.js'); + compareResults(result1, result2); }); +}); - var es6 = () => require('./ReactES6Class-test.js'); - var coffee = () => require('./ReactCoffeeScriptClass-test.coffee'); - var ts = () => require('./ReactTypeScriptClass-test.ts'); +function runJest(testFile) { + var cwd = process.cwd(); + var jestBin = path.resolve('node_modules', '.bin', 'jest'); + var setupFile = path.resolve( + 'scripts', + 'jest', + 'setupSpecEquivalenceReporter.js', + ); + var result = spawnSync( + 'node', + [jestBin, testFile, '--setupTestFrameworkScriptFile', setupFile], + {cwd}, + ); - it('tests the same thing for es6 classes and CoffeeScript', function() { - expect(coffee).toEqualSpecsIn(es6); - }); + if (result.error) { + throw result.error; + } - it('tests the same thing for es6 classes and TypeScript', function() { - expect(ts).toEqualSpecsIn(es6); - }); + if (result.status !== 0) { + throw new Error( + 'jest process exited with: ' + + result.status + + '\n' + + 'stdout: ' + + result.stdout.toString() + + 'stderr: ' + + result.stderr.toString(), + ); + } -}); + return result.stdout.toString(); +} + +function compareResults(a, b) { + var regexp = /EQUIVALENCE.*$/gm; + var aSpecs = (a.match(regexp) || []).sort().join('\n'); + var bSpecs = (b.match(regexp) || []).sort().join('\n'); + + if (aSpecs.length === 0 && bSpecs.length === 0) { + throw new Error('No spec results found in the output'); + } + + expect(aSpecs).toEqual(bSpecs); +} diff --git a/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee b/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee index e7b1f574c..23661be18 100644 --- a/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/src/isomorphic/modern/class/__tests__/ReactCoffeeScriptClass-test.coffee @@ -9,2 +9,3 @@ of patent rights can be found in the PATENTS file in the same directory. +PropTypes = null React = null @@ -23,2 +24,3 @@ describe 'ReactCoffeeScriptClass', -> ReactDOM = require 'ReactDOM' + PropTypes = require 'prop-types' container = document.createElement 'div' @@ -53,4 +55,5 @@ describe 'ReactCoffeeScriptClass', -> ).toThrow() - expect(console.error.calls.length).toBe(1) - expect(console.error.argsForCall[0][0]).toContain('No `render` method found on the returned component instance') + expect(console.error.calls.count()).toBe(1) + expect(console.error.calls.argsFor(0)[0]).toContain('No `render` method found on the returned component instance') + undefined @@ -64,2 +67,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo, bar: 'bar'), 'DIV', 'bar' + undefined @@ -76,2 +80,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'foo' + undefined @@ -96,2 +101,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo), 'SPAN', 'bar' + undefined @@ -100,4 +106,4 @@ describe 'ReactCoffeeScriptClass', -> @contextTypes: - tag: React.PropTypes.string - className: React.PropTypes.string + tag: PropTypes.string + className: PropTypes.string @@ -116,4 +122,4 @@ describe 'ReactCoffeeScriptClass', -> @childContextTypes: - tag: React.PropTypes.string - className: React.PropTypes.string + tag: PropTypes.string + className: PropTypes.string @@ -127,2 +133,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Outer), 'SPAN', 'foo' + undefined @@ -143,2 +150,3 @@ describe 'ReactCoffeeScriptClass', -> expect(renderCount).toBe 1 + undefined @@ -155,5 +163,6 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo), 'span', '' - ).toThrow( + ).toThrowError( 'Foo.state: must be set to an object or null' ) + undefined @@ -168,2 +177,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo), 'SPAN', '' + undefined @@ -185,2 +195,3 @@ describe 'ReactCoffeeScriptClass', -> expect(renderedName).toBe 'bar' + undefined @@ -201,2 +212,3 @@ describe 'ReactCoffeeScriptClass', -> expect(attachedListener).toThrow() + undefined @@ -219,2 +231,3 @@ describe 'ReactCoffeeScriptClass', -> expect(renderedName).toBe 'bar' + undefined @@ -268,2 +281,3 @@ describe 'ReactCoffeeScriptClass', -> expect(lifeCycles).toEqual ['will-unmount'] + undefined @@ -294,15 +308,35 @@ describe 'ReactCoffeeScriptClass', -> expect(getDefaultPropsWasCalled).toBe false - expect(console.error.calls.length).toBe 4 - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe 4 + expect(console.error.calls.argsFor(0)[0]).toContain( 'getInitialState was defined on Foo, a plain JavaScript class.' ) - expect(console.error.argsForCall[1][0]).toContain( + expect(console.error.calls.argsFor(1)[0]).toContain( 'getDefaultProps was defined on Foo, a plain JavaScript class.' ) - expect(console.error.argsForCall[2][0]).toContain( + expect(console.error.calls.argsFor(2)[0]).toContain( 'propTypes was defined as an instance property on Foo.' ) - expect(console.error.argsForCall[3][0]).toContain( + expect(console.error.calls.argsFor(3)[0]).toContain( 'contextTypes was defined as an instance property on Foo.' ) + undefined + + it 'does not warn about getInitialState() on class components + if state is also defined.', -> + spyOn console, 'error' + class Foo extends React.Component + constructor: (props) -> + super props + @state = bar: @props.initialValue + + getInitialState: -> + {} + + render: -> + span + className: 'foo' + + test React.createElement(Foo), 'SPAN', 'foo' + expect(console.error.calls.count()).toBe 0 + undefined @@ -319,4 +353,4 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(NamedComponent), 'SPAN', 'foo' - expect(console.error.calls.length).toBe 1 - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe 1 + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: NamedComponent has a method called componentShouldUpdate(). @@ -325,2 +359,3 @@ describe 'ReactCoffeeScriptClass', -> ) + undefined @@ -337,4 +372,4 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(NamedComponent), 'SPAN', 'foo' - expect(console.error.calls.length).toBe 1 - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe 1 + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: NamedComponent has a method called componentWillRecieveProps(). @@ -342,5 +377,6 @@ describe 'ReactCoffeeScriptClass', -> ) + undefined it 'should throw AND warn when trying to access classic APIs', -> - spyOn console, 'error' + spyOn console, 'warn' instance = @@ -349,11 +385,10 @@ describe 'ReactCoffeeScriptClass', -> expect(-> instance.isMounted()).toThrow() - expect(-> instance.setProps name: 'bar').toThrow() - expect(-> instance.replaceProps name: 'bar').toThrow() - expect(console.error.calls.length).toBe 2 - expect(console.error.argsForCall[0][0]).toContain( + expect(console.warn.calls.count()).toBe 2 + expect(console.warn.calls.argsFor(0)[0]).toContain( 'replaceState(...) is deprecated in plain JavaScript React classes' ) - expect(console.error.argsForCall[1][0]).toContain( + expect(console.warn.calls.argsFor(1)[0]).toContain( 'isMounted(...) is deprecated in plain JavaScript React classes' ) + undefined @@ -362,3 +397,3 @@ describe 'ReactCoffeeScriptClass', -> @contextTypes: - bar: React.PropTypes.string + bar: PropTypes.string render: -> @@ -368,3 +403,3 @@ describe 'ReactCoffeeScriptClass', -> @childContextTypes: - bar: React.PropTypes.string + bar: PropTypes.string getChildContext: -> @@ -375,2 +410,3 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo), 'DIV', 'bar-through-context' + undefined @@ -385,2 +421,3 @@ describe 'ReactCoffeeScriptClass', -> expect(instance.refs.inner.getName()).toBe 'foo' + undefined @@ -390 +427,2 @@ describe 'ReactCoffeeScriptClass', -> expect(node).toBe container.firstChild + undefined diff --git a/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js b/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js index 17a2414c5..ee63a7af7 100644 --- a/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactES6Class-test.js @@ -13,2 +13,3 @@ +var PropTypes; var React; @@ -16,4 +17,3 @@ var ReactDOM; -describe('ReactES6Class', function() { - +describe('ReactES6Class', () => { var container; @@ -27,5 +27,6 @@ describe('ReactES6Class', function() { - beforeEach(function() { + beforeEach(() => { React = require('React'); ReactDOM = require('ReactDOM'); + PropTypes = require('prop-types'); container = document.createElement('div'); @@ -53,4 +54,4 @@ describe('ReactES6Class', function() { - it('preserves the name of the class for use in error messages', function() { - class Foo extends React.Component { } + it('preserves the name of the class for use in error messages', () => { + class Foo extends React.Component {} expect(Foo.name).toBe('Foo'); @@ -58,11 +59,11 @@ describe('ReactES6Class', function() { - it('throws if no render function is defined', function() { + it('throws if no render function is defined', () => { spyOn(console, 'error'); - class Foo extends React.Component { } + class Foo extends React.Component {} expect(() => ReactDOM.render(, container)).toThrow(); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: Foo(...): No `render` method found on the returned component ' + - 'instance: you may have forgotten to define `render`.' + 'instance: you may have forgotten to define `render`.', ); @@ -70,3 +71,3 @@ describe('ReactES6Class', function() { - it('renders a simple stateless component with prop', function() { + it('renders a simple stateless component with prop', () => { class Foo extends React.Component { @@ -80,3 +81,3 @@ describe('ReactES6Class', function() { - it('renders based on state using initial values in this.props', function() { + it('renders based on state using initial values in this.props', () => { class Foo extends React.Component { @@ -93,3 +94,3 @@ describe('ReactES6Class', function() { - it('renders based on state using props in the constructor', function() { + it('renders based on state using props in the constructor', () => { class Foo extends React.Component { @@ -114,3 +115,3 @@ describe('ReactES6Class', function() { - it('renders based on context in the constructor', function() { + it('renders based on context in the constructor', () => { class Foo extends React.Component { @@ -126,4 +127,4 @@ describe('ReactES6Class', function() { Foo.contextTypes = { - tag: React.PropTypes.string, - className: React.PropTypes.string, + tag: PropTypes.string, + className: PropTypes.string, }; @@ -139,4 +140,4 @@ describe('ReactES6Class', function() { Outer.childContextTypes = { - tag: React.PropTypes.string, - className: React.PropTypes.string, + tag: PropTypes.string, + className: PropTypes.string, }; @@ -145,3 +146,3 @@ describe('ReactES6Class', function() { - it('renders only once when setting state in componentWillMount', function() { + it('renders only once when setting state in componentWillMount', () => { var renderCount = 0; @@ -164,3 +165,3 @@ describe('ReactES6Class', function() { - it('should throw with non-object in the initial state property', function() { + it('should throw with non-object in the initial state property', () => { [['an array'], 'a string', 1234].forEach(function(state) { @@ -175,4 +176,4 @@ describe('ReactES6Class', function() { } - expect(() => test(, 'span', '')).toThrow( - 'Foo.state: must be set to an object or null' + expect(() => test(, 'span', '')).toThrowError( + 'Foo.state: must be set to an object or null', ); @@ -181,3 +182,3 @@ describe('ReactES6Class', function() { - it('should render with null in the initial state property', function() { + it('should render with null in the initial state property', () => { class Foo extends React.Component { @@ -194,3 +195,3 @@ describe('ReactES6Class', function() { - it('setState through an event handler', function() { + it('setState through an event handler', () => { class Foo extends React.Component { @@ -205,6 +206,3 @@ describe('ReactES6Class', function() { return ( - + ); @@ -217,3 +215,3 @@ describe('ReactES6Class', function() { - it('should not implicitly bind event handlers', function() { + it('should not implicitly bind event handlers', () => { class Foo extends React.Component { @@ -227,8 +225,3 @@ describe('ReactES6Class', function() { render() { - return ( - - ); + return ; } @@ -239,3 +232,3 @@ describe('ReactES6Class', function() { - it('renders using forceUpdate even when there is no state', function() { + it('renders using forceUpdate even when there is no state', () => { class Foo extends React.Component { @@ -263,3 +256,3 @@ describe('ReactES6Class', function() { - it('will call all the normal life cycle methods', function() { + it('will call all the normal life cycle methods', () => { var lifeCycles = []; @@ -297,6 +290,3 @@ describe('ReactES6Class', function() { test(, 'SPAN', 'foo'); - expect(lifeCycles).toEqual([ - 'will-mount', - 'did-mount', - ]); + expect(lifeCycles).toEqual(['will-mount', 'did-mount']); lifeCycles = []; // reset @@ -304,6 +294,13 @@ describe('ReactES6Class', function() { expect(lifeCycles).toEqual([ - 'receive-props', freeze({value: 'bar'}), - 'should-update', freeze({value: 'bar'}), {}, - 'will-update', freeze({value: 'bar'}), {}, - 'did-update', freeze({value: 'foo'}), {}, + 'receive-props', + freeze({value: 'bar'}), + 'should-update', + freeze({value: 'bar'}), + {}, + 'will-update', + freeze({value: 'bar'}), + {}, + 'did-update', + freeze({value: 'foo'}), + {}, ]); @@ -311,8 +308,6 @@ describe('ReactES6Class', function() { ReactDOM.unmountComponentAtNode(container); - expect(lifeCycles).toEqual([ - 'will-unmount', - ]); + expect(lifeCycles).toEqual(['will-unmount']); }); - it('warns when classic properties are defined on the instance, but does not invoke them.', function() { + it('warns when classic properties are defined on the instance, but does not invoke them.', () => { spyOn(console, 'error'); @@ -341,14 +336,14 @@ describe('ReactES6Class', function() { expect(getDefaultPropsWasCalled).toBe(false); - expect(console.error.calls.length).toBe(4); - expect(console.error.argsForCall[0][0]).toContain( - 'getInitialState was defined on Foo, a plain JavaScript class.' + expect(console.error.calls.count()).toBe(4); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'getInitialState was defined on Foo, a plain JavaScript class.', ); - expect(console.error.argsForCall[1][0]).toContain( - 'getDefaultProps was defined on Foo, a plain JavaScript class.' + expect(console.error.calls.argsFor(1)[0]).toContain( + 'getDefaultProps was defined on Foo, a plain JavaScript class.', ); - expect(console.error.argsForCall[2][0]).toContain( - 'propTypes was defined as an instance property on Foo.' + expect(console.error.calls.argsFor(2)[0]).toContain( + 'propTypes was defined as an instance property on Foo.', ); - expect(console.error.argsForCall[3][0]).toContain( - 'contextTypes was defined as an instance property on Foo.' + expect(console.error.calls.argsFor(3)[0]).toContain( + 'contextTypes was defined as an instance property on Foo.', ); @@ -356,3 +351,18 @@ describe('ReactES6Class', function() { - it('should warn when misspelling shouldComponentUpdate', function() { + it('does not warn about getInitialState() on class components if state is also defined.', () => { + spyOn(console, 'error'); + class Foo extends React.Component { + state = this.getInitialState(); + getInitialState() { + return {}; + } + render() { + return ; + } + } + test(, 'SPAN', 'foo'); + expect(console.error.calls.count()).toBe(0); + }); + + it('should warn when misspelling shouldComponentUpdate', () => { spyOn(console, 'error'); @@ -369,8 +379,8 @@ describe('ReactES6Class', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: ' + - 'NamedComponent has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' + 'NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', ); @@ -378,3 +388,3 @@ describe('ReactES6Class', function() { - it('should warn when misspelling componentWillReceiveProps', function() { + it('should warn when misspelling componentWillReceiveProps', () => { spyOn(console, 'error'); @@ -391,7 +401,7 @@ describe('ReactES6Class', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: ' + - 'NamedComponent has a method called componentWillRecieveProps(). Did ' + - 'you mean componentWillReceiveProps()?' + 'NamedComponent has a method called componentWillRecieveProps(). Did ' + + 'you mean componentWillReceiveProps()?', ); @@ -399,4 +409,4 @@ describe('ReactES6Class', function() { - it('should throw AND warn when trying to access classic APIs', function() { - spyOn(console, 'error'); + it('should throw AND warn when trying to access classic APIs', () => { + spyOn(console, 'warn'); var instance = test(, 'DIV', 'foo'); @@ -404,10 +414,8 @@ describe('ReactES6Class', function() { expect(() => instance.isMounted()).toThrow(); - expect(() => instance.setProps({name: 'bar'})).toThrow(); - expect(() => instance.replaceProps({name: 'bar'})).toThrow(); - expect(console.error.calls.length).toBe(2); - expect(console.error.argsForCall[0][0]).toContain( - 'replaceState(...) is deprecated in plain JavaScript React classes' + expect(console.warn.calls.count()).toBe(2); + expect(console.warn.calls.argsFor(0)[0]).toContain( + 'replaceState(...) is deprecated in plain JavaScript React classes', ); - expect(console.error.argsForCall[1][0]).toContain( - 'isMounted(...) is deprecated in plain JavaScript React classes' + expect(console.warn.calls.argsFor(1)[0]).toContain( + 'isMounted(...) is deprecated in plain JavaScript React classes', ); @@ -415,3 +423,3 @@ describe('ReactES6Class', function() { - it('supports this.context passed via getChildContext', function() { + it('supports this.context passed via getChildContext', () => { class Bar extends React.Component { @@ -421,3 +429,3 @@ describe('ReactES6Class', function() { } - Bar.contextTypes = {bar: React.PropTypes.string}; + Bar.contextTypes = {bar: PropTypes.string}; class Foo extends React.Component { @@ -430,3 +438,3 @@ describe('ReactES6Class', function() { } - Foo.childContextTypes = {bar: React.PropTypes.string}; + Foo.childContextTypes = {bar: PropTypes.string}; test(, 'DIV', 'bar-through-context'); @@ -434,3 +442,3 @@ describe('ReactES6Class', function() { - it('supports classic refs', function() { + it('supports classic refs', () => { class Foo extends React.Component { @@ -444,3 +452,3 @@ describe('ReactES6Class', function() { - it('supports drilling through to the DOM using findDOMNode', function() { + it('supports drilling through to the DOM using findDOMNode', () => { var instance = test(, 'DIV', 'foo'); @@ -449,3 +457,2 @@ describe('ReactES6Class', function() { }); - }); diff --git a/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js new file mode 100644 index 000000000..0cf7f751a --- /dev/null +++ b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js @@ -0,0 +1,96 @@ +/** + * Copyright 2013-present, 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 React; +var ReactDOM; + +describe('ReactPureComponent', () => { + beforeEach(() => { + React = require('React'); + ReactDOM = require('ReactDOM'); + }); + + it('should render', () => { + var renders = 0; + class Component extends React.PureComponent { + constructor() { + super(); + this.state = {type: 'mushrooms'}; + } + render() { + renders++; + return
{this.props.text[0]}
; + } + } + + var container = document.createElement('div'); + var text; + var component; + + text = ['porcini']; + component = ReactDOM.render(, container); + expect(container.textContent).toBe('porcini'); + expect(renders).toBe(1); + + text = ['morel']; + component = ReactDOM.render(, container); + expect(container.textContent).toBe('morel'); + expect(renders).toBe(2); + + text[0] = 'portobello'; + component = ReactDOM.render(, container); + expect(container.textContent).toBe('morel'); + expect(renders).toBe(2); + + // Setting state without changing it doesn't cause a rerender. + component.setState({type: 'mushrooms'}); + expect(container.textContent).toBe('morel'); + expect(renders).toBe(2); + + // But changing state does. + component.setState({type: 'portobello mushrooms'}); + expect(container.textContent).toBe('portobello'); + expect(renders).toBe(3); + }); + + it('can override shouldComponentUpdate', () => { + var renders = 0; + class Component extends React.PureComponent { + render() { + renders++; + return
; + } + shouldComponentUpdate() { + return true; + } + } + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + expect(renders).toBe(2); + }); + + it('extends React.Component', () => { + var renders = 0; + class Component extends React.PureComponent { + render() { + expect(this instanceof React.Component).toBe(true); + expect(this instanceof React.PureComponent).toBe(true); + renders++; + return
; + } + } + ReactDOM.render(, document.createElement('div')); + expect(renders).toBe(1); + }); +}); diff --git a/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts b/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts index 0078265bc..d7de7469d 100644 --- a/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts +++ b/src/isomorphic/modern/class/__tests__/ReactTypeScriptClass-test.ts @@ -1 +1,5 @@ +/// +/// +/// + /*! @@ -11,2 +15,3 @@ import React = require('React'); import ReactDOM = require('ReactDOM'); +import PropTypes = require('prop-types'); @@ -84,4 +89,4 @@ class StateBasedOnContext extends React.Component { static contextTypes = { - tag: React.PropTypes.string, - className: React.PropTypes.string + tag: PropTypes.string, + className: PropTypes.string }; @@ -99,4 +104,4 @@ class ProvideChildContextTypes extends React.Component { static childContextTypes = { - tag: React.PropTypes.string, - className: React.PropTypes.string + tag: PropTypes.string, + className: PropTypes.string }; @@ -277,3 +282,3 @@ class MisspelledComponent2 extends React.Component { class ReadContext extends React.Component { - static contextTypes = { bar: React.PropTypes.string }; + static contextTypes = { bar: PropTypes.string }; render() { @@ -283,3 +288,3 @@ class ReadContext extends React.Component { class ProvideContext extends React.Component { - static childContextTypes = { bar: React.PropTypes.string }; + static childContextTypes = { bar: PropTypes.string }; getChildContext() { @@ -318,4 +323,4 @@ describe('ReactTypeScriptClass', function() { - expect((console.error).argsForCall.length).toBe(1); - expect((console.error).argsForCall[0][0]).toBe( + expect((console.error).calls.count()).toBe(1); + expect((console.error).calls.argsFor(0)[0]).toBe( 'Warning: Empty(...): No `render` method found on the returned ' + @@ -360,3 +365,3 @@ describe('ReactTypeScriptClass', function() { expect(() => test(React.createElement(ArrayState), 'span', '')) - .toThrow( + .toThrowError( 'ArrayState.state: must be set to an object or null' @@ -364,3 +369,3 @@ describe('ReactTypeScriptClass', function() { expect(() => test(React.createElement(StringState), 'span', '')) - .toThrow( + .toThrowError( 'StringState.state: must be set to an object or null' @@ -368,3 +373,3 @@ describe('ReactTypeScriptClass', function() { expect(() => test(React.createElement(NumberState), 'span', '')) - .toThrow( + .toThrowError( 'NumberState.state: must be set to an object or null' @@ -436,4 +441,4 @@ describe('ReactTypeScriptClass', function() { expect(getDefaultPropsWasCalled).toBe(false); - expect((console.error).argsForCall.length).toBe(4); - expect((console.error).argsForCall[0][0]).toContain( + expect((console.error).calls.count()).toBe(4); + expect((console.error).calls.argsFor(0)[0]).toContain( 'getInitialState was defined on ClassicProperties, ' + @@ -441,3 +446,3 @@ describe('ReactTypeScriptClass', function() { ); - expect((console.error).argsForCall[1][0]).toContain( + expect((console.error).calls.argsFor(1)[0]).toContain( 'getDefaultProps was defined on ClassicProperties, ' + @@ -445,6 +450,6 @@ describe('ReactTypeScriptClass', function() { ); - expect((console.error).argsForCall[2][0]).toContain( + expect((console.error).calls.argsFor(2)[0]).toContain( 'propTypes was defined as an instance property on ClassicProperties.' ); - expect((console.error).argsForCall[3][0]).toContain( + expect((console.error).calls.argsFor(3)[0]).toContain( 'contextTypes was defined as an instance property on ClassicProperties.' @@ -453,2 +458,20 @@ describe('ReactTypeScriptClass', function() { + it('does not warn about getInitialState() on class components ' + + 'if state is also defined.', () => { + spyOn(console, 'error'); + + class Example extends React.Component { + state = {}; + getInitialState() { + return {}; + } + render() { + return React.createElement('span', {className: 'foo'}); + } + } + + test(React.createElement(Example), 'SPAN', 'foo'); + expect((console.error).calls.count()).toBe(0); + }); + it('should warn when misspelling shouldComponentUpdate', function() { @@ -458,4 +481,4 @@ describe('ReactTypeScriptClass', function() { - expect((console.error).argsForCall.length).toBe(1); - expect((console.error).argsForCall[0][0]).toBe( + expect((console.error).calls.count()).toBe(1); + expect((console.error).calls.argsFor(0)[0]).toBe( 'Warning: ' + @@ -472,4 +495,4 @@ describe('ReactTypeScriptClass', function() { - expect((console.error).argsForCall.length).toBe(1); - expect((console.error).argsForCall[0][0]).toBe( + expect((console.error).calls.count()).toBe(1); + expect((console.error).calls.argsFor(0)[0]).toBe( 'Warning: ' + @@ -481,3 +504,3 @@ describe('ReactTypeScriptClass', function() { it('should throw AND warn when trying to access classic APIs', function() { - spyOn(console, 'error'); + spyOn(console, 'warn'); var instance = test( @@ -488,9 +511,7 @@ describe('ReactTypeScriptClass', function() { expect(() => instance.isMounted()).toThrow(); - expect(() => instance.setProps({ name: 'bar' })).toThrow(); - expect(() => instance.replaceProps({ name: 'bar' })).toThrow(); - expect((console.error).argsForCall.length).toBe(2); - expect((console.error).argsForCall[0][0]).toContain( + expect((console.warn).calls.count()).toBe(2); + expect((console.warn).calls.argsFor(0)[0]).toContain( 'replaceState(...) is deprecated in plain JavaScript React classes' ); - expect((console.error).argsForCall[1][0]).toContain( + expect((console.warn).calls.argsFor(1)[0]).toContain( 'isMounted(...) is deprecated in plain JavaScript React classes' diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js index 95126ca11..fc494b436 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js @@ -17,6 +17,6 @@ var ReactTestUtils; -describe('ReactJSXElement', function() { +describe('ReactJSXElement', () => { var Component; - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -33,3 +33,3 @@ describe('ReactJSXElement', function() { - it('returns a complete element according to spec', function() { + it('returns a complete element according to spec', () => { var element = ; @@ -43,3 +43,3 @@ describe('ReactJSXElement', function() { - it('allows a lower-case to be passed as the string type', function() { + it('allows a lower-case to be passed as the string type', () => { var element =
; @@ -53,3 +53,3 @@ describe('ReactJSXElement', function() { - it('allows a string to be passed as the type', function() { + it('allows a string to be passed as the type', () => { var TagName = 'div'; @@ -64,8 +64,8 @@ describe('ReactJSXElement', function() { - it('returns an immutable element', function() { + it('returns an immutable element', () => { var element = ; - expect(() => element.type = 'div').toThrow(); + expect(() => (element.type = 'div')).toThrow(); }); - it('does not reuse the object that is spread into props', function() { + it('does not reuse the object that is spread into props', () => { var config = {foo: 1}; @@ -77,3 +77,3 @@ describe('ReactJSXElement', function() { - it('extracts key and ref from the rest of the props', function() { + it('extracts key and ref from the rest of the props', () => { var element = ; @@ -82,3 +82,3 @@ describe('ReactJSXElement', function() { expect(element.ref).toBe('34'); - var expectation = {foo:'56'}; + var expectation = {foo: '56'}; Object.freeze(expectation); @@ -87,3 +87,3 @@ describe('ReactJSXElement', function() { - it('coerces the key to a string', function() { + it('coerces the key to a string', () => { var element = ; @@ -92,3 +92,3 @@ describe('ReactJSXElement', function() { expect(element.ref).toBe(null); - var expectation = {foo:'56'}; + var expectation = {foo: '56'}; Object.freeze(expectation); @@ -97,3 +97,3 @@ describe('ReactJSXElement', function() { - it('merges JSX children onto the children prop', function() { + it('merges JSX children onto the children prop', () => { spyOn(console, 'error'); @@ -102,6 +102,6 @@ describe('ReactJSXElement', function() { expect(element.props.children).toBe(a); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not override children if no JSX children are provided', function() { + it('does not override children if no JSX children are provided', () => { spyOn(console, 'error'); @@ -109,6 +109,6 @@ describe('ReactJSXElement', function() { expect(element.props.children).toBe('text'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('overrides children if null is provided as a JSX child', function() { + it('overrides children if null is provided as a JSX child', () => { spyOn(console, 'error'); @@ -116,6 +116,18 @@ describe('ReactJSXElement', function() { expect(element.props.children).toBe(null); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('merges JSX children onto the children prop in an array', function() { + it('overrides children if undefined is provided as an argument', () => { + var element = {undefined}; + expect(element.props.children).toBe(undefined); + + var element2 = React.cloneElement( + , + {}, + undefined, + ); + expect(element2.props.children).toBe(undefined); + }); + + it('merges JSX children onto the children prop in an array', () => { spyOn(console, 'error'); @@ -126,6 +138,6 @@ describe('ReactJSXElement', function() { expect(element.props.children).toEqual([1, 2, 3]); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('allows static methods to be called using the type property', function() { + it('allows static methods to be called using the type property', () => { spyOn(console, 'error'); @@ -137,3 +149,3 @@ describe('ReactJSXElement', function() { render() { - return
; + return
; } @@ -143,6 +155,6 @@ describe('ReactJSXElement', function() { expect(element.type.someStaticMethod()).toBe('someReturnValue'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('identifies valid elements', function() { + it('identifies valid elements', () => { expect(React.isValidElement(
)).toEqual(true); @@ -155,6 +167,6 @@ describe('ReactJSXElement', function() { expect(React.isValidElement(Component)).toEqual(false); - expect(React.isValidElement({ type: 'div', props: {} })).toEqual(false); + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); }); - it('is indistinguishable from a plain object', function() { + it('is indistinguishable from a plain object', () => { var element =
; @@ -164,3 +176,3 @@ describe('ReactJSXElement', function() { - it('should use default prop value when removing a prop', function() { + it('should use default prop value when removing a prop', () => { Component.defaultProps = {fruit: 'persimmon'}; @@ -168,6 +180,3 @@ describe('ReactJSXElement', function() { var container = document.createElement('div'); - var instance = ReactDOM.render( - , - container - ); + var instance = ReactDOM.render(, container); expect(instance.props.fruit).toBe('mango'); @@ -178,3 +187,3 @@ describe('ReactJSXElement', function() { - it('should normalize props with default values', function() { + it('should normalize props with default values', () => { class NormalizingComponent extends React.Component { @@ -189,7 +198,7 @@ describe('ReactJSXElement', function() { - var inst2 = - ReactTestUtils.renderIntoDocument(); + var inst2 = ReactTestUtils.renderIntoDocument( + , + ); expect(inst2.props.prop).toBe(null); }); - }); diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 1e2890e9f..0502161aa 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -16,6 +16,12 @@ +var PropTypes; var React; +var ReactDOM; var ReactTestUtils; -describe('ReactJSXElementValidator', function() { +describe('ReactJSXElementValidator', () => { + function normalizeCodeLocInfo(str) { + return str && str.replace(/at .+?:\d+/g, 'at **'); + } + var Component; @@ -23,3 +29,3 @@ describe('ReactJSXElementValidator', function() { - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -27,3 +33,5 @@ describe('ReactJSXElementValidator', function() { React = require('React'); + ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); + PropTypes = require('prop-types'); @@ -41,6 +49,6 @@ describe('ReactJSXElementValidator', function() { RequiredPropComponent.displayName = 'RequiredPropComponent'; - RequiredPropComponent.propTypes = {prop: React.PropTypes.string.isRequired}; + RequiredPropComponent.propTypes = {prop: PropTypes.string.isRequired}; }); - it('warns for keys for arrays of elements in children position', function() { + it('warns for keys for arrays of elements in children position', () => { spyOn(console, 'error'); @@ -49,5 +57,5 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.', ); @@ -55,3 +63,3 @@ describe('ReactJSXElementValidator', function() { - it('warns for keys for arrays of elements with owner info', function() { + it('warns for keys for arrays of elements with owner info', () => { spyOn(console, 'error'); @@ -66,7 +74,3 @@ describe('ReactJSXElementValidator', function() { render() { - return ( - , ]} - /> - ); + return , ]} />; } @@ -76,7 +80,7 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(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. ' + 'Check the render method of `InnerComponent`. ' + + 'It was passed a child from ComponentWrapper. ', ); @@ -84,3 +88,3 @@ describe('ReactJSXElementValidator', function() { - it('warns for keys for iterables of elements in rest args', function() { + it('warns for keys for iterables of elements in rest args', () => { spyOn(console, 'error'); @@ -101,5 +105,5 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Each child in an array or iterator should have a unique "key" prop.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Each child in an array or iterator should have a unique "key" prop.', ); @@ -107,11 +111,13 @@ describe('ReactJSXElementValidator', function() { - it('does not warns for arrays of elements with keys', function() { + it('does not warns for arrays of elements with keys', () => { spyOn(console, 'error'); - void {[, ]}; + void ( + {[, ]} + ); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warns for iterable elements with keys', function() { + it('does not warns for iterable elements with keys', () => { spyOn(console, 'error'); @@ -135,6 +141,6 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn for numeric keys in entry iterable as a child', function() { + it('does not warn for numeric keys in entry iterable as a child', () => { spyOn(console, 'error'); @@ -156,6 +162,6 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the element is directly as children', function() { + it('does not warn when the element is directly as children', () => { spyOn(console, 'error'); @@ -164,6 +170,6 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('does not warn when the child array contains non-elements', function() { + it('does not warn when the child array contains non-elements', () => { spyOn(console, 'error'); @@ -172,3 +178,3 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); @@ -189,3 +195,3 @@ describe('ReactJSXElementValidator', function() { MyComp.propTypes = { - color: React.PropTypes.string, + color: PropTypes.string, }; @@ -197,6 +203,45 @@ describe('ReactJSXElementValidator', function() { ReactTestUtils.renderIntoDocument(); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + - 'expected `string`. Check the render method of `ParentComp`.' + 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: 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 **)', ); @@ -215,24 +260,33 @@ describe('ReactJSXElementValidator', function() { void ; - expect(console.error.calls.length).toBe(4); - expect(console.error.argsForCall[0][0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + expect(console.error.calls.count()).toBe(4); + 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. " + + 'Check your code at **.', ); - expect(console.error.argsForCall[1][0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + 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. ' + + 'Check your code at **.', ); - expect(console.error.argsForCall[2][0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + 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. ' + + 'Check your code at **.', ); - expect(console.error.argsForCall[3][0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + 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. ' + + 'Check your code at **.', ); void
; - expect(console.error.calls.length).toBe(4); + expect(console.error.calls.count()).toBe(4); }); - it('should check default prop values', function() { + it('should check default prop values', () => { spyOn(console, 'error'); @@ -243,6 +297,7 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `RequiredPropComponent`.' + expect(console.error.calls.count()).toBe(1); + 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 **)', ); @@ -250,3 +305,3 @@ describe('ReactJSXElementValidator', function() { - it('should not check the default for explicit null', function() { + it('should not check the default for explicit null', () => { spyOn(console, 'error'); @@ -255,6 +310,7 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `RequiredPropComponent`.' + expect(console.error.calls.count()).toBe(1); + 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 **)', ); @@ -262,3 +318,3 @@ describe('ReactJSXElementValidator', function() { - it('should check declared prop types', function() { + it('should check declared prop types', () => { spyOn(console, 'error'); @@ -268,12 +324,15 @@ describe('ReactJSXElementValidator', function() { - expect(console.error.calls.length).toBe(2); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + - 'Required prop `prop` was not specified in `RequiredPropComponent`.' + expect(console.error.calls.count()).toBe(2); + 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.argsForCall[1][0]).toBe( - 'Warning: Failed propType: ' + - 'Invalid prop `prop` of type `number` supplied to ' + - '`RequiredPropComponent`, expected `string`.' + 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' + + ' in RequiredPropComponent (at **)', ); @@ -283,6 +342,6 @@ describe('ReactJSXElementValidator', function() { // Should not error for strings - expect(console.error.calls.length).toBe(2); + expect(console.error.calls.count()).toBe(2); }); - it('should warn on invalid prop types', function() { + it('should warn on invalid prop types', () => { // Since there is no prevalidation step for ES6 classes, there is no hook @@ -301,6 +360,6 @@ describe('ReactJSXElementValidator', function() { ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'NullPropTypeComponent: prop type `prop` is invalid; it must be a ' + - 'function, usually from React.PropTypes.' + 'function, usually from React.PropTypes.', ); @@ -308,3 +367,3 @@ describe('ReactJSXElementValidator', function() { - it('should warn on invalid context types', function() { + it('should warn on invalid context types', () => { spyOn(console, 'error'); @@ -319,6 +378,6 @@ describe('ReactJSXElementValidator', function() { ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'NullContextTypeComponent: context type `prop` is invalid; it must ' + - 'be a function, usually from React.PropTypes.' + 'be a function, usually from React.PropTypes.', ); @@ -326,3 +385,3 @@ describe('ReactJSXElementValidator', function() { - it('should warn if getDefaultProps is specificed on the class', function() { + it('should warn if getDefaultProps is specificed on the class', () => { spyOn(console, 'error'); @@ -337,6 +396,6 @@ describe('ReactJSXElementValidator', function() { ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( 'getDefaultProps is only used on classic React.createClass definitions.' + - ' Use a static property named `defaultProps` instead.' + ' Use a static property named `defaultProps` instead.', ); @@ -344,2 +403,52 @@ describe('ReactJSXElementValidator', function() { + it('provides stack via non-standard console.reactStack for invalid types', () => { + spyOn(console, 'error'); + + function Foo() { + var Bad = undefined; + return ; + } + + function App() { + return
; + } + + try { + console.reactStack = jest.fn(); + console.reactStackEnd = jest.fn(); + + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow( + 'Element 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. Check the render method of `Foo`.', + ); + + expect(console.reactStack.mock.calls.length).toBe(1); + expect(console.reactStackEnd.mock.calls.length).toBe(1); + + var stack = console.reactStack.mock.calls[0][0]; + expect(Array.isArray(stack)).toBe(true); + expect(stack.map(frame => frame.name)).toEqual([ + 'Foo', // is inside Foo + 'App', // is inside App + 'App', //
is inside App + null, // is outside a component + ]); + expect( + stack.map(frame => frame.fileName && frame.fileName.slice(-8)), + ).toEqual(['-test.js', '-test.js', '-test.js', '-test.js']); + expect(stack.map(frame => typeof frame.lineNumber)).toEqual([ + 'number', + 'number', + 'number', + 'number', + ]); + } finally { + delete console.reactStack; + delete console.reactStackEnd; + } + }); }); diff --git a/src/node_modules/react/lib/getNextDebugID.js b/src/node_modules/react/lib/getNextDebugID.js new file mode 100644 index 000000000..e7e3d8ca6 --- /dev/null +++ b/src/node_modules/react/lib/getNextDebugID.js @@ -0,0 +1,9 @@ +/** + * * Copyright 2016-present Facebook. All Rights Reserved. + * * + * * @flow + * */ + +'use strict'; + +module.exports = require('getNextDebugID'); diff --git a/src/package.json b/src/package.json index 8279f8f6d..9d50d45d1 100644 --- a/src/package.json +++ b/src/package.json @@ -2,3 +2,3 @@ "name": "react-haste", - "version": "15.0.0", + "version": "15.0.1", "license": "BSD-3-Clause" diff --git a/src/renderers/art/ReactART.js b/src/renderers/art/ReactART.js new file mode 100644 index 000000000..7550e330d --- /dev/null +++ b/src/renderers/art/ReactART.js @@ -0,0 +1,630 @@ +/** + * Copyright (c) 2013-present 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. + * + * @providesModule ReactART + */ + +'use strict'; + +require('art/modes/current').setCurrent( + require('art/modes/fast-noSideEffects'), // Flip this to DOM mode for debugging +); + +const Transform = require('art/core/transform'); +const Mode = require('art/modes/current'); + +const React = require('React'); +const ReactDOM = require('ReactDOM'); +const ReactInstanceMap = require('ReactInstanceMap'); +const ReactMultiChild = require('ReactMultiChild'); +const ReactUpdates = require('ReactUpdates'); + +const createReactClass = require('createClass'); +const emptyObject = require('emptyObject'); +const invariant = require('invariant'); + +const assign = require('object-assign'); +const pooledTransform = new Transform(); + +// Utilities + +function childrenAsString(children) { + if (!children) { + return ''; + } + if (typeof children === 'string') { + return children; + } + if (children.length) { + return children.join('\n'); + } + return ''; +} + +function createComponent(name) { + const ReactARTComponent = function(element) { + this.node = null; + this.subscriptions = null; + this.listeners = null; + this._mountImage = null; + this._renderedChildren = null; + this.construct(element); + }; + ReactARTComponent.displayName = name; + for (let i = 1, l = arguments.length; i < l; i++) { + assign(ReactARTComponent.prototype, arguments[i]); + } + + return ReactARTComponent; +} + +/** + * Insert `node` into `parentNode` after `referenceNode`. + */ +function injectAfter(parentNode, referenceNode, node) { + let beforeNode; + if ( + node.parentNode === parentNode && + node.previousSibling === referenceNode + ) { + return; + } + if (referenceNode == null) { + // node is supposed to be first. + beforeNode = parentNode.firstChild; + } else { + // node is supposed to be after referenceNode. + beforeNode = referenceNode.nextSibling; + } + if (beforeNode && beforeNode.previousSibling !== node) { + // Cases where `node === beforeNode` should get filtered out by earlier + // checks and the behavior isn't well-defined. + invariant( + node !== beforeNode, + 'ReactART: Can not insert node before itself', + ); + node.injectBefore(beforeNode); + } else if (node.parentNode !== parentNode) { + node.inject(parentNode); + } +} + +// ContainerMixin for components that can hold ART nodes + +const ContainerMixin = assign({}, ReactMultiChild.Mixin, { + /** + * Moves a child component to the supplied index. + * + * @param {ReactComponent} child Component to move. + * @param {number} toIndex Destination index of the element. + * @protected + */ + moveChild: function(child, afterNode, toIndex, lastIndex) { + const childNode = child._mountImage; + injectAfter(this.node, afterNode, childNode); + }, + + /** + * Creates a child component. + * + * @param {ReactComponent} child Component to create. + * @param {object} childNode ART node to insert. + * @protected + */ + createChild: function(child, afterNode, childNode) { + child._mountImage = childNode; + injectAfter(this.node, afterNode, childNode); + }, + + /** + * Removes a child component. + * + * @param {ReactComponent} child Child to remove. + * @protected + */ + removeChild: function(child) { + child._mountImage.eject(); + child._mountImage = null; + }, + + updateChildrenAtRoot: function(nextChildren, transaction) { + this.updateChildren(nextChildren, transaction, emptyObject); + }, + + mountAndInjectChildrenAtRoot: function(children, transaction) { + this.mountAndInjectChildren(children, transaction, emptyObject); + }, + + /** + * Override to bypass batch updating because it is not necessary. + * + * @param {?object} nextChildren. + * @param {ReactReconcileTransaction} transaction + * @internal + * @override {ReactMultiChild.Mixin.updateChildren} + */ + updateChildren: function(nextChildren, transaction, context) { + this._updateChildren(nextChildren, transaction, context); + }, + + // Shorthands + + mountAndInjectChildren: function(children, transaction, context) { + const mountedImages = this.mountChildren(children, transaction, context); + // Each mount image corresponds to one of the flattened children + let i = 0; + for (let key in this._renderedChildren) { + if (this._renderedChildren.hasOwnProperty(key)) { + const child = this._renderedChildren[key]; + child._mountImage = mountedImages[i]; + mountedImages[i].inject(this.node); + i++; + } + } + }, +}); + +// Surface is a React DOM Component, not an ART component. It serves as the +// entry point into the ART reconciler. + +const Surface = createReactClass({ + displayName: 'Surface', + + mixins: [ContainerMixin], + + componentDidMount: function() { + const domNode = ReactDOM.findDOMNode(this); + + this.node = Mode.Surface(+this.props.width, +this.props.height, domNode); + + const transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + this.mountAndInjectChildren, + this, + this.props.children, + transaction, + ReactInstanceMap.get(this)._context, + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); + }, + + componentDidUpdate: function(oldProps) { + const node = this.node; + if ( + this.props.width != oldProps.width || + this.props.height != oldProps.height + ) { + node.resize(+this.props.width, +this.props.height); + } + + const transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + this.updateChildren, + this, + this.props.children, + transaction, + ReactInstanceMap.get(this)._context, + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); + + if (node.render) { + node.render(); + } + }, + + componentWillUnmount: function() { + this.unmountChildren(); + }, + + render: function() { + // This is going to be a placeholder because we don't know what it will + // actually resolve to because ART may render canvas, vml or svg tags here. + // We only allow a subset of properties since others might conflict with + // ART's properties. + const props = this.props; + + // TODO: ART's Canvas Mode overrides surface title and cursor + const Tag = Mode.Surface.tagName; + return ( + + ); + }, +}); + +// Various nodes that can go into a surface + +const EventTypes = { + onMouseMove: 'mousemove', + onMouseOver: 'mouseover', + onMouseOut: 'mouseout', + onMouseUp: 'mouseup', + onMouseDown: 'mousedown', + onClick: 'click', +}; + +const NodeMixin = { + construct: function(element) { + this._currentElement = element; + }, + + getNativeNode: function() { + return this.node; + }, + + getPublicInstance: function() { + return this.node; + }, + + putEventListener: function(type, listener) { + const subscriptions = this.subscriptions || (this.subscriptions = {}); + const listeners = this.listeners || (this.listeners = {}); + listeners[type] = listener; + if (listener) { + if (!subscriptions[type]) { + subscriptions[type] = this.node.subscribe(type, listener, this); + } + } else { + if (subscriptions[type]) { + subscriptions[type](); + delete subscriptions[type]; + } + } + }, + + handleEvent: function(event) { + const listener = this.listeners[event.type]; + if (!listener) { + return; + } + if (typeof listener === 'function') { + listener.call(this, event); + } else if (listener.handleEvent) { + listener.handleEvent(event); + } + }, + + destroyEventListeners: function() { + const subscriptions = this.subscriptions; + if (subscriptions) { + for (let type in subscriptions) { + subscriptions[type](); + } + } + this.subscriptions = null; + this.listeners = null; + }, + + applyNodeProps: function(oldProps, props) { + const node = this.node; + + const scaleX = props.scaleX != null + ? props.scaleX + : props.scale != null ? props.scale : 1; + const scaleY = props.scaleY != null + ? props.scaleY + : props.scale != null ? props.scale : 1; + + pooledTransform + .transformTo(1, 0, 0, 1, 0, 0) + .move(props.x || 0, props.y || 0) + .rotate(props.rotation || 0, props.originX, props.originY) + .scale(scaleX, scaleY, props.originX, props.originY); + + if (props.transform != null) { + pooledTransform.transform(props.transform); + } + + if ( + node.xx !== pooledTransform.xx || + node.yx !== pooledTransform.yx || + node.xy !== pooledTransform.xy || + node.yy !== pooledTransform.yy || + node.x !== pooledTransform.x || + node.y !== pooledTransform.y + ) { + node.transformTo(pooledTransform); + } + + if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) { + node.indicate(props.cursor, props.title); + } + + if (node.blend && props.opacity !== oldProps.opacity) { + node.blend(props.opacity == null ? 1 : props.opacity); + } + + if (props.visible !== oldProps.visible) { + if (props.visible == null || props.visible) { + node.show(); + } else { + node.hide(); + } + } + + for (let type in EventTypes) { + this.putEventListener(EventTypes[type], props[type]); + } + }, + + mountComponentIntoNode: function(rootID, container) { + throw new Error( + 'You cannot render an ART component standalone. ' + + 'You need to wrap it in a Surface.', + ); + }, +}; + +// Group + +const Group = createComponent('Group', NodeMixin, ContainerMixin, { + mountComponent: function( + transaction, + nativeParent, + nativeContainerInfo, + context, + ) { + this.node = Mode.Group(); + const props = this._currentElement.props; + this.applyGroupProps(emptyObject, props); + this.mountAndInjectChildren(props.children, transaction, context); + return this.node; + }, + + receiveComponent: function(nextComponent, transaction, context) { + const props = nextComponent.props; + const oldProps = this._currentElement.props; + this.applyGroupProps(oldProps, props); + this.updateChildren(props.children, transaction, context); + this._currentElement = nextComponent; + }, + + applyGroupProps: function(oldProps, props) { + this.node.width = props.width; + this.node.height = props.height; + this.applyNodeProps(oldProps, props); + }, + + unmountComponent: function() { + this.destroyEventListeners(); + this.unmountChildren(); + }, +}); + +// ClippingRectangle +const ClippingRectangle = createComponent( + 'ClippingRectangle', + NodeMixin, + ContainerMixin, + { + mountComponent: function( + transaction, + nativeParent, + nativeContainerInfo, + context, + ) { + this.node = Mode.ClippingRectangle(); + const props = this._currentElement.props; + this.applyClippingProps(emptyObject, props); + this.mountAndInjectChildren(props.children, transaction, context); + return this.node; + }, + + receiveComponent: function(nextComponent, transaction, context) { + const props = nextComponent.props; + const oldProps = this._currentElement.props; + this.applyClippingProps(oldProps, props); + this.updateChildren(props.children, transaction, context); + this._currentElement = nextComponent; + }, + + applyClippingProps: function(oldProps, props) { + this.node.width = props.width; + this.node.height = props.height; + this.node.x = props.x; + this.node.y = props.y; + this.applyNodeProps(oldProps, props); + }, + + unmountComponent: function() { + this.destroyEventListeners(); + this.unmountChildren(); + }, + }, +); + +// Renderables + +const RenderableMixin = assign({}, NodeMixin, { + applyRenderableProps: function(oldProps, props) { + if (oldProps.fill !== props.fill) { + if (props.fill && props.fill.applyFill) { + props.fill.applyFill(this.node); + } else { + this.node.fill(props.fill); + } + } + if ( + oldProps.stroke !== props.stroke || + oldProps.strokeWidth !== props.strokeWidth || + oldProps.strokeCap !== props.strokeCap || + oldProps.strokeJoin !== props.strokeJoin || + // TODO: Consider a deep check of stokeDash. + // This may benefit the VML version in IE. + oldProps.strokeDash !== props.strokeDash + ) { + this.node.stroke( + props.stroke, + props.strokeWidth, + props.strokeCap, + props.strokeJoin, + props.strokeDash, + ); + } + this.applyNodeProps(oldProps, props); + }, + + unmountComponent: function() { + this.destroyEventListeners(); + }, +}); + +// Shape + +const Shape = createComponent('Shape', RenderableMixin, { + construct: function(element) { + this._currentElement = element; + this._oldDelta = null; + this._oldPath = null; + }, + + mountComponent: function( + transaction, + nativeParent, + nativeContainerInfo, + context, + ) { + this.node = Mode.Shape(); + const props = this._currentElement.props; + this.applyShapeProps(emptyObject, props); + return this.node; + }, + + receiveComponent: function(nextComponent, transaction, context) { + const props = nextComponent.props; + const oldProps = this._currentElement.props; + this.applyShapeProps(oldProps, props); + this._currentElement = nextComponent; + }, + + applyShapeProps: function(oldProps, props) { + const oldDelta = this._oldDelta; + const oldPath = this._oldPath; + const path = props.d || childrenAsString(props.children); + + if ( + path.delta !== oldDelta || + path !== oldPath || + oldProps.width !== props.width || + oldProps.height !== props.height + ) { + this.node.draw(path, props.width, props.height); + + this._oldPath = path; + this._oldDelta = path.delta; + } + + this.applyRenderableProps(oldProps, props); + }, +}); + +// Text + +const Text = createComponent('Text', RenderableMixin, { + construct: function(element) { + this._currentElement = element; + this._oldString = null; + }, + + mountComponent: function( + transaction, + nativeParent, + nativeContainerInfo, + context, + ) { + const props = this._currentElement.props; + const newString = childrenAsString(props.children); + this.node = Mode.Text(newString, props.font, props.alignment, props.path); + this._oldString = newString; + this.applyRenderableProps(emptyObject, props); + return this.node; + }, + + isSameFont: function(oldFont, newFont) { + if (oldFont === newFont) { + return true; + } + if (typeof newFont === 'string' || typeof oldFont === 'string') { + return false; + } + return ( + newFont.fontSize === oldFont.fontSize && + newFont.fontStyle === oldFont.fontStyle && + newFont.fontVariant === oldFont.fontVariant && + newFont.fontWeight === oldFont.fontWeight && + newFont.fontFamily === oldFont.fontFamily + ); + }, + + receiveComponent: function(nextComponent, transaction, context) { + const props = nextComponent.props; + const oldProps = this._currentElement.props; + + const oldString = this._oldString; + const newString = childrenAsString(props.children); + + if ( + oldString !== newString || + !this.isSameFont(oldProps.font, props.font) || + oldProps.alignment !== props.alignment || + oldProps.path !== props.path + ) { + this.node.draw(newString, props.font, props.alignment, props.path); + this._oldString = newString; + } + + this.applyRenderableProps(oldProps, props); + this._currentElement = nextComponent; + }, +}); + +// Declarative fill type objects - API design not finalized + +const slice = Array.prototype.slice; + +function LinearGradient(stops, x1, y1, x2, y2) { + this.args = slice.call(arguments); +} + +LinearGradient.prototype.applyFill = function(node) { + node.fillLinear.apply(node, this.args); +}; + +function RadialGradient(stops, fx, fy, rx, ry, cx, cy) { + this.args = slice.call(arguments); +} + +RadialGradient.prototype.applyFill = function(node) { + node.fillRadial.apply(node, this.args); +}; + +function Pattern(url, width, height, left, top) { + this.args = slice.call(arguments); +} + +Pattern.prototype.applyFill = function(node) { + node.fillImage.apply(node, this.args); +}; + +module.exports = { + ClippingRectangle, + Group, + LinearGradient, + Path: Mode.Path, + Pattern, + RadialGradient, + Shape, + Surface, + Text, + Transform, +}; diff --git a/src/renderers/art/__tests__/ReactART-test.js b/src/renderers/art/__tests__/ReactART-test.js new file mode 100644 index 000000000..7d9ce37ff --- /dev/null +++ b/src/renderers/art/__tests__/ReactART-test.js @@ -0,0 +1,301 @@ +/** + * Copyright (c) 2013-present 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 + */ + +/*jslint evil: true */ + +'use strict'; + +var React = require('React'); +var ReactDOM = require('ReactDOM'); +var ReactTestUtils = require('ReactTestUtils'); + +var Group; +var Shape; +var Surface; +var TestComponent; + +var Missing = {}; + +var ReactART = require('ReactART'); +var ARTSVGMode = require('art/modes/svg'); +var ARTCurrentMode = require('art/modes/current'); + +function testDOMNodeStructure(domNode, expectedStructure) { + expect(domNode).toBeDefined(); + expect(domNode.nodeName).toBe(expectedStructure.nodeName); + for (var prop in expectedStructure) { + if (!expectedStructure.hasOwnProperty(prop)) continue; + if (prop != 'nodeName' && prop != 'children') { + if (expectedStructure[prop] === Missing) { + expect(domNode.hasAttribute(prop)).toBe(false); + } else { + expect(domNode.getAttribute(prop)).toBe(expectedStructure[prop]); + } + } + } + if (expectedStructure.children) { + expectedStructure.children.forEach(function(subTree, index) { + testDOMNodeStructure(domNode.childNodes[index], subTree); + }); + } +} + +describe('ReactART', () => { + beforeEach(() => { + ARTCurrentMode.setCurrent(ARTSVGMode); + + Group = ReactART.Group; + Shape = ReactART.Shape; + Surface = ReactART.Surface; + + TestComponent = class extends React.Component { + render() { + var a = ( + + ); + + var b = ( + + M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666 + h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994 + h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z + + ); + + var c = ; + + return ( + + + {this.props.flipped ? [b, a, c] : [a, b, c]} + + + ); + } + }; + }); + + it('should have the correct lifecycle state', () => { + var instance = ; + instance = ReactTestUtils.renderIntoDocument(instance); + var group = instance.refs.group; + // Duck type test for an ART group + expect(typeof group.indicate).toBe('function'); + }); + + it('should render a reasonable SVG structure in SVG mode', () => { + var instance = ; + instance = ReactTestUtils.renderIntoDocument(instance); + + var expectedStructure = { + nodeName: 'svg', + width: '150', + height: '200', + children: [ + {nodeName: 'defs'}, + { + nodeName: 'g', + children: [ + { + nodeName: 'defs', + children: [{nodeName: 'linearGradient'}], + }, + {nodeName: 'path'}, + {nodeName: 'path'}, + {nodeName: 'g'}, + ], + }, + ], + }; + + var realNode = ReactDOM.findDOMNode(instance); + testDOMNodeStructure(realNode, expectedStructure); + }); + + it('should be able to reorder components', () => { + var container = document.createElement('div'); + var instance = ReactDOM.render( + , + container, + ); + + var expectedStructure = { + nodeName: 'svg', + children: [ + {nodeName: 'defs'}, + { + nodeName: 'g', + children: [ + {nodeName: 'defs'}, + {nodeName: 'path', opacity: '0.1'}, + {nodeName: 'path', opacity: Missing}, + {nodeName: 'g'}, + ], + }, + ], + }; + + var realNode = ReactDOM.findDOMNode(instance); + testDOMNodeStructure(realNode, expectedStructure); + + ReactDOM.render(, container); + + var expectedNewStructure = { + nodeName: 'svg', + children: [ + {nodeName: 'defs'}, + { + nodeName: 'g', + children: [ + {nodeName: 'defs'}, + {nodeName: 'path', opacity: Missing}, + {nodeName: 'path', opacity: '0.1'}, + {nodeName: 'g'}, + ], + }, + ], + }; + + testDOMNodeStructure(realNode, expectedNewStructure); + }); + + it('should be able to reorder many components', () => { + var container = document.createElement('div'); + + class Component extends React.Component { + render() { + var chars = this.props.chars.split(''); + return ( + + {chars.map(text => )} + + ); + } + } + + // Mini multi-child stress test: lots of reorders, some adds, some removes. + var before = 'abcdefghijklmnopqrst'; + var after = 'mxhpgwfralkeoivcstzy'; + + var instance = ReactDOM.render(, container); + var realNode = ReactDOM.findDOMNode(instance); + expect(realNode.textContent).toBe(before); + + instance = ReactDOM.render(, container); + expect(realNode.textContent).toBe(after); + + ReactDOM.unmountComponentAtNode(container); + }); + + it('renders composite with lifecycle inside group', () => { + var mounted = false; + + class CustomShape extends React.Component { + render() { + return ; + } + + componentDidMount() { + mounted = true; + } + } + + ReactTestUtils.renderIntoDocument( + + + + + , + ); + expect(mounted).toBe(true); + }); + + it('resolves refs before componentDidMount', () => { + class CustomShape extends React.Component { + render() { + return ; + } + } + + var ref = null; + + class Outer extends React.Component { + componentDidMount() { + ref = this.refs.test; + } + + render() { + return ( + + + + + + ); + } + } + + ReactTestUtils.renderIntoDocument(); + expect(ref.constructor).toBe(CustomShape); + }); + + it('resolves refs before componentDidUpdate', () => { + class CustomShape extends React.Component { + render() { + return ; + } + } + + var ref = {}; + + class Outer extends React.Component { + componentDidMount() { + ref = this.refs.test; + } + + componentDidUpdate() { + ref = this.refs.test; + } + + render() { + return ( + + + {this.props.mountCustomShape && } + + + ); + } + } + + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(ref).not.toBeDefined(); + ReactDOM.render(, container); + expect(ref.constructor).toBe(CustomShape); + }); +}); diff --git a/src/renderers/dom/ReactDOM.js b/src/renderers/dom/ReactDOM.js index 75644b120..a30ee9af4 100644 --- a/src/renderers/dom/ReactDOM.js +++ b/src/renderers/dom/ReactDOM.js @@ -18,3 +18,2 @@ var ReactDefaultInjection = require('ReactDefaultInjection'); var ReactMount = require('ReactMount'); -var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); @@ -24,3 +23,3 @@ var ReactVersion = require('ReactVersion'); var findDOMNode = require('findDOMNode'); -var getNativeComponentFromComposite = require('getNativeComponentFromComposite'); +var getHostComponentFromComposite = require('getHostComponentFromComposite'); var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer'); @@ -30,7 +29,5 @@ ReactDefaultInjection.inject(); -var render = ReactPerf.measure('React', 'render', ReactMount.render); - -var React = { +var ReactDOM = { findDOMNode: findDOMNode, - render: render, + render: ReactMount.render, unmountComponentAtNode: ReactMount.unmountComponentAtNode, @@ -48,3 +45,4 @@ if ( typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && - typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') { + typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function' +) { __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ @@ -56,3 +54,3 @@ if ( if (inst._renderedComponent) { - inst = getNativeComponentFromComposite(inst); + inst = getHostComponentFromComposite(inst); } @@ -73,3 +71,2 @@ if (__DEV__) { if (ExecutionEnvironment.canUseDOM && window.top === window.self) { - // First check if devtools is not installed @@ -77,7 +74,10 @@ if (__DEV__) { // If we're in Chrome or Firefox, provide a download link if not installed. - if ((navigator.userAgent.indexOf('Chrome') > -1 && + if ( + (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1) || - navigator.userAgent.indexOf('Firefox') > -1) { + navigator.userAgent.indexOf('Firefox') > -1 + ) { // Firefox does not have the issue with devtools loaded over file:// - var showFileUrlMessage = window.location.protocol.indexOf('http') === -1 && + var showFileUrlMessage = + window.location.protocol.indexOf('http') === -1 && navigator.userAgent.indexOf('Firefox') === -1; @@ -85,5 +85,7 @@ if (__DEV__) { 'Download the React DevTools ' + - (showFileUrlMessage ? 'and use an HTTP server (instead of a file: URL) ' : '') + - 'for a better development experience: ' + - 'https://fb.me/react-devtools' + (showFileUrlMessage + ? 'and use an HTTP server (instead of a file: URL) ' + : '') + + 'for a better development experience: ' + + 'https://fb.me/react-devtools', ); @@ -95,6 +97,6 @@ if (__DEV__) { (testFunc.name || testFunc.toString()).indexOf('testFn') !== -1, - 'It looks like you\'re using a minified copy of the development build ' + - 'of React. When deploying React apps to production, make sure to use ' + - 'the production build which skips development warnings and is faster. ' + - 'See https://fb.me/react-minification for more details.' + "It looks like you're using a minified copy of the development build " + + 'of React. When deploying React apps to production, make sure to use ' + + 'the production build which skips development warnings and is faster. ' + + 'See https://fb.me/react-minification for more details.', ); @@ -109,4 +111,4 @@ if (__DEV__) { 'Internet Explorer is running in compatibility mode; please add the ' + - 'following tag to your HTML to prevent this from happening: ' + - '' + 'following tag to your HTML to prevent this from happening: ' + + '', ); @@ -123,3 +125,2 @@ if (__DEV__) { Object.keys, - String.prototype.split, String.prototype.trim, @@ -132,3 +133,3 @@ if (__DEV__) { 'One or more ES5 shims expected by React are not available: ' + - 'https://fb.me/react-warning-polyfills' + 'https://fb.me/react-warning-polyfills', ); @@ -140,2 +141,13 @@ if (__DEV__) { -module.exports = React; +if (__DEV__) { + var ReactInstrumentation = require('ReactInstrumentation'); + var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook'); + var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook'); + var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook'); + + ReactInstrumentation.debugTool.addHook(ReactDOMUnknownPropertyHook); + ReactInstrumentation.debugTool.addHook(ReactDOMNullInputValuePropHook); + ReactInstrumentation.debugTool.addHook(ReactDOMInvalidARIAHook); +} + +module.exports = ReactDOM; diff --git a/src/renderers/dom/__mocks__/ReactDOM.js b/src/renderers/dom/__mocks__/ReactDOM.js new file mode 100644 index 000000000..c5965c9ec --- /dev/null +++ b/src/renderers/dom/__mocks__/ReactDOM.js @@ -0,0 +1,18 @@ +/** + * Copyright 2013-2015, 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. + */ + +'use strict'; + +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + +var useFiber = ReactDOMFeatureFlags.useFiber; + +module.exports = useFiber + ? require('ReactDOMFiber') + : require.requireActual('ReactDOM'); diff --git a/src/renderers/dom/__tests__/ReactDOMProduction-test.js b/src/renderers/dom/__tests__/ReactDOMProduction-test.js index dd5a96a98..f2b4a7fc1 100644 --- a/src/renderers/dom/__tests__/ReactDOMProduction-test.js +++ b/src/renderers/dom/__tests__/ReactDOMProduction-test.js @@ -10,6 +10,5 @@ */ - 'use strict'; -describe('ReactDOMProduction', function() { +describe('ReactDOMProduction', () => { var oldProcess; @@ -19,3 +18,3 @@ describe('ReactDOMProduction', function() { - beforeEach(function() { + beforeEach(() => { __DEV__ = false; @@ -29,3 +28,3 @@ describe('ReactDOMProduction', function() { - afterEach(function() { + afterEach(() => { __DEV__ = true; @@ -34,3 +33,3 @@ describe('ReactDOMProduction', function() { - it('should use prod fbjs', function() { + it('should use prod fbjs', () => { var warning = require('warning'); @@ -39,6 +38,6 @@ describe('ReactDOMProduction', function() { warning(false, 'Do cows go moo?'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('should use prod React', function() { + it('should use prod React', () => { spyOn(console, 'error'); @@ -48,11 +47,11 @@ describe('ReactDOMProduction', function() { - expect(console.error.argsForCall.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('should handle a simple flow', function() { - var Component = React.createClass({ - render: function() { + it('should handle a simple flow', () => { + class Component extends React.Component { + render() { return {this.props.children}; - }, - }); + } + } @@ -65,3 +64,3 @@ describe('ReactDOMProduction', function() {
, - container + container, ); @@ -78,3 +77,3 @@ describe('ReactDOMProduction', function() {
, - container + container, ); @@ -89,2 +88,94 @@ describe('ReactDOMProduction', function() { + it('should call lifecycle methods', () => { + var log = []; + class Component extends React.Component { + state = {y: 1}; + shouldComponentUpdate(nextProps, nextState) { + log.push(['shouldComponentUpdate', nextProps, nextState]); + return nextProps.x !== this.props.x || nextState.y !== this.state.y; + } + componentWillMount() { + log.push(['componentWillMount']); + } + componentDidMount() { + log.push(['componentDidMount']); + } + componentWillReceiveProps(nextProps) { + log.push(['componentWillReceiveProps', nextProps]); + } + componentWillUpdate(nextProps, nextState) { + log.push(['componentWillUpdate', nextProps, nextState]); + } + componentDidUpdate(prevProps, prevState) { + log.push(['componentDidUpdate', prevProps, prevState]); + } + componentWillUnmount() { + log.push(['componentWillUnmount']); + } + render() { + log.push(['render']); + return null; + } + } + + var container = document.createElement('div'); + var inst = ReactDOM.render(, container); + expect(log).toEqual([ + ['componentWillMount'], + ['render'], + ['componentDidMount'], + ]); + log = []; + + inst.setState({y: 2}); + expect(log).toEqual([ + ['shouldComponentUpdate', {x: 1}, {y: 2}], + ['componentWillUpdate', {x: 1}, {y: 2}], + ['render'], + ['componentDidUpdate', {x: 1}, {y: 1}], + ]); + log = []; + + inst.setState({y: 2}); + expect(log).toEqual([['shouldComponentUpdate', {x: 1}, {y: 2}]]); + log = []; + + ReactDOM.render(, container); + expect(log).toEqual([ + ['componentWillReceiveProps', {x: 2}], + ['shouldComponentUpdate', {x: 2}, {y: 2}], + ['componentWillUpdate', {x: 2}, {y: 2}], + ['render'], + ['componentDidUpdate', {x: 1}, {y: 2}], + ]); + log = []; + + ReactDOM.render(, container); + expect(log).toEqual([ + ['componentWillReceiveProps', {x: 2}], + ['shouldComponentUpdate', {x: 2}, {y: 2}], + ]); + log = []; + + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([['componentWillUnmount']]); + }); + + it('should throw with an error code in production', () => { + expect(function() { + class Component extends React.Component { + render() { + return ['this is wrong']; + } + } + + var container = document.createElement('div'); + ReactDOM.render(, container); + }).toThrowError( + 'Minified React error #109; visit ' + + 'http://facebook.github.io/react/docs/error-decoder.html?invariant=109&args[]=Component' + + ' for the full message or use the non-minified dev environment' + + ' for full errors and additional helpful warnings.', + ); + }); }); diff --git a/src/renderers/dom/client/ReactBrowserEventEmitter.js b/src/renderers/dom/client/ReactBrowserEventEmitter.js index dd7334f48..e51837c11 100644 --- a/src/renderers/dom/client/ReactBrowserEventEmitter.js +++ b/src/renderers/dom/client/ReactBrowserEventEmitter.js @@ -13,3 +13,2 @@ -var EventConstants = require('EventConstants'); var EventPluginRegistry = require('EventPluginRegistry'); @@ -87,4 +86,6 @@ var topEventMapping = { topAnimationEnd: getVendorPrefixedEventName('animationend') || 'animationend', - topAnimationIteration: getVendorPrefixedEventName('animationiteration') || 'animationiteration', - topAnimationStart: getVendorPrefixedEventName('animationstart') || 'animationstart', + topAnimationIteration: + getVendorPrefixedEventName('animationiteration') || 'animationiteration', + topAnimationStart: + getVendorPrefixedEventName('animationstart') || 'animationstart', topBlur: 'blur', @@ -145,3 +146,4 @@ var topEventMapping = { topTouchStart: 'touchstart', - topTransitionEnd: getVendorPrefixedEventName('transitionend') || 'transitionend', + topTransitionEnd: + getVendorPrefixedEventName('transitionend') || 'transitionend', topVolumeChange: 'volumechange', @@ -177,3 +179,2 @@ function getListeningForDocument(mountAt) { var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { - /** @@ -189,3 +190,3 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { ReactEventListener.setHandleTopLevel( - ReactBrowserEventEmitter.handleTopLevel + ReactBrowserEventEmitter.handleTopLevel, ); @@ -243,15 +244,13 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { - var topLevelTypes = EventConstants.topLevelTypes; for (var i = 0; i < dependencies.length; i++) { var dependency = dependencies[i]; - if (!( - isListening.hasOwnProperty(dependency) && - isListening[dependency] - )) { - if (dependency === topLevelTypes.topWheel) { + if ( + !(isListening.hasOwnProperty(dependency) && isListening[dependency]) + ) { + if (dependency === 'topWheel') { if (isEventSupported('wheel')) { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topWheel, + 'topWheel', 'wheel', - mountAt + mountAt, ); @@ -259,5 +258,5 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topWheel, + 'topWheel', 'mousewheel', - mountAt + mountAt, ); @@ -267,14 +266,13 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topWheel, + 'topWheel', 'DOMMouseScroll', - mountAt + mountAt, ); } - } else if (dependency === topLevelTypes.topScroll) { - + } else if (dependency === 'topScroll') { if (isEventSupported('scroll', true)) { ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( - topLevelTypes.topScroll, + 'topScroll', 'scroll', - mountAt + mountAt, ); @@ -282,20 +280,18 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topScroll, + 'topScroll', 'scroll', - ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE + ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE, ); } - } else if (dependency === topLevelTypes.topFocus || - dependency === topLevelTypes.topBlur) { - + } else if (dependency === 'topFocus' || dependency === 'topBlur') { if (isEventSupported('focus', true)) { ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( - topLevelTypes.topFocus, + 'topFocus', 'focus', - mountAt + mountAt, ); ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( - topLevelTypes.topBlur, + 'topBlur', 'blur', - mountAt + mountAt, ); @@ -305,10 +301,10 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topFocus, + 'topFocus', 'focusin', - mountAt + mountAt, ); ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent( - topLevelTypes.topBlur, + 'topBlur', 'focusout', - mountAt + mountAt, ); @@ -317,4 +313,4 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { // to make sure blur and focus event listeners are only attached once - isListening[topLevelTypes.topBlur] = true; - isListening[topLevelTypes.topFocus] = true; + isListening.topBlur = true; + isListening.topFocus = true; } else if (topEventMapping.hasOwnProperty(dependency)) { @@ -323,3 +319,3 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { topEventMapping[dependency], - mountAt + mountAt, ); @@ -336,3 +332,3 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { handlerBaseName, - handle + handle, ); @@ -344,3 +340,3 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { handlerBaseName, - handle + handle, ); @@ -348,2 +344,15 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { + /** + * Protect against document.createEvent() returning null + * Some popup blocker extensions appear to do this: + * https://github.com/facebook/react/issues/6887 + */ + supportsEventPageXY: function() { + if (!document.createEvent) { + return false; + } + var ev = document.createEvent('MouseEvent'); + return ev != null && 'pageX' in ev; + }, + /** @@ -361,4 +370,3 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { if (hasEventPageXY === undefined) { - hasEventPageXY = - document.createEvent && 'pageX' in document.createEvent('MouseEvent'); + hasEventPageXY = ReactBrowserEventEmitter.supportsEventPageXY(); } @@ -370,3 +378,2 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, { }, - }); diff --git a/src/renderers/dom/client/ReactDOMComponentTree.js b/src/renderers/dom/client/ReactDOMComponentTree.js index ec95b26c6..88e2b4bf1 100644 --- a/src/renderers/dom/client/ReactDOMComponentTree.js +++ b/src/renderers/dom/client/ReactDOMComponentTree.js @@ -25,4 +25,16 @@ var internalInstanceKey = /** - * Drill down (through composites and empty components) until we get a native or - * native text component. + * Check if a given node should be cached. + */ +function shouldPrecacheNode(node, nodeID) { + return ( + (node.nodeType === 1 && node.getAttribute(ATTR_NAME) === String(nodeID)) || + (node.nodeType === 8 && + node.nodeValue === ' react-text: ' + nodeID + ' ') || + (node.nodeType === 8 && node.nodeValue === ' react-empty: ' + nodeID + ' ') + ); +} + +/** + * Drill down (through composites and empty components) until we get a host or + * host text component. * @@ -31,3 +43,3 @@ var internalInstanceKey = */ -function getRenderedNativeOrTextFromComponent(component) { +function getRenderedHostOrTextFromComponent(component) { var rendered; @@ -40,3 +52,3 @@ function getRenderedNativeOrTextFromComponent(component) { /** - * Populate `_nativeNode` on the rendered native/text component with the given + * Populate `_hostNode` on the rendered host/text component with the given * DOM node. The passed `inst` can be a composite. @@ -44,5 +56,5 @@ function getRenderedNativeOrTextFromComponent(component) { function precacheNode(inst, node) { - var nativeInst = getRenderedNativeOrTextFromComponent(inst); - nativeInst._nativeNode = node; - node[internalInstanceKey] = nativeInst; + var hostInst = getRenderedHostOrTextFromComponent(inst); + hostInst._hostNode = node; + node[internalInstanceKey] = hostInst; } @@ -50,6 +62,6 @@ function precacheNode(inst, node) { function uncacheNode(inst) { - var node = inst._nativeNode; + var node = inst._hostNode; if (node) { delete node[internalInstanceKey]; - inst._nativeNode = null; + inst._hostNode = null; } @@ -58,3 +70,3 @@ function uncacheNode(inst) { /** - * Populate `_nativeNode` on each child of `inst`, assuming that the children + * Populate `_hostNode` on each child of `inst`, assuming that the children * match up with the DOM (element) children of `node`. @@ -82,4 +94,4 @@ function precacheChildNodes(inst, node) { var childInst = children[name]; - var childID = getRenderedNativeOrTextFromComponent(childInst)._domID; - if (childID == null) { + var childID = getRenderedHostOrTextFromComponent(childInst)._domID; + if (childID === 0) { // We're currently unmounting this child in ReactMultiChild; skip it. @@ -89,8 +101,3 @@ function precacheChildNodes(inst, node) { for (; childNode !== null; childNode = childNode.nextSibling) { - if ((childNode.nodeType === 1 && - childNode.getAttribute(ATTR_NAME) === String(childID)) || - (childNode.nodeType === 8 && - childNode.nodeValue === ' react-text: ' + childID + ' ') || - (childNode.nodeType === 8 && - childNode.nodeValue === ' react-empty: ' + childID + ' ')) { + if (shouldPrecacheNode(childNode, childID)) { precacheNode(childInst, childNode); @@ -145,3 +152,3 @@ function getInstanceFromNode(node) { var inst = getClosestInstanceFromNode(node); - if (inst != null && inst._nativeNode === node) { + if (inst != null && inst._hostNode === node) { return inst; @@ -160,8 +167,8 @@ function getNodeFromInstance(inst) { invariant( - inst._nativeNode !== undefined, - 'getNodeFromInstance: Invalid argument.' + inst._hostNode !== undefined, + 'getNodeFromInstance: Invalid argument.', ); - if (inst._nativeNode) { - return inst._nativeNode; + if (inst._hostNode) { + return inst._hostNode; } @@ -170,9 +177,9 @@ function getNodeFromInstance(inst) { var parents = []; - while (!inst._nativeNode) { + while (!inst._hostNode) { parents.push(inst); invariant( - inst._nativeParent, - 'React DOM tree root should always have a node reference.' + inst._hostParent, + 'React DOM tree root should always have a node reference.', ); - inst = inst._nativeParent; + inst = inst._hostParent; } @@ -182,6 +189,6 @@ function getNodeFromInstance(inst) { for (; parents.length; inst = parents.pop()) { - precacheChildNodes(inst, inst._nativeNode); + precacheChildNodes(inst, inst._hostNode); } - return inst._nativeNode; + return inst._hostNode; } diff --git a/src/renderers/dom/client/ReactDOMIDOperations.js b/src/renderers/dom/client/ReactDOMIDOperations.js index cb03d3f06..e24335341 100644 --- a/src/renderers/dom/client/ReactDOMIDOperations.js +++ b/src/renderers/dom/client/ReactDOMIDOperations.js @@ -15,3 +15,2 @@ var DOMChildrenOperations = require('DOMChildrenOperations'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var ReactPerf = require('ReactPerf'); @@ -21,3 +20,2 @@ var ReactPerf = require('ReactPerf'); var ReactDOMIDOperations = { - /** @@ -34,6 +32,2 @@ var ReactDOMIDOperations = { -ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', { - dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates', -}); - module.exports = ReactDOMIDOperations; diff --git a/src/renderers/dom/client/ReactDOMSelection.js b/src/renderers/dom/client/ReactDOMSelection.js index aff9e894b..307d5c340 100644 --- a/src/renderers/dom/client/ReactDOMSelection.js +++ b/src/renderers/dom/client/ReactDOMSelection.js @@ -101,3 +101,3 @@ function getModernOffsets(node) { selection.focusNode, - selection.focusOffset + selection.focusOffset, ); @@ -114,3 +114,3 @@ function getModernOffsets(node) { tempRange.endContainer, - tempRange.endOffset + tempRange.endOffset, ); @@ -178,4 +178,3 @@ function setModernOffsets(node, offsets) { var start = Math.min(offsets.start, length); - var end = offsets.end === undefined ? - start : Math.min(offsets.end, length); + var end = offsets.end === undefined ? start : Math.min(offsets.end, length); @@ -207,7 +206,6 @@ function setModernOffsets(node, offsets) { -var useIEOffsets = ( +var useIEOffsets = ExecutionEnvironment.canUseDOM && 'selection' in document && - !('getSelection' in window) -); + !('getSelection' in window); diff --git a/src/renderers/dom/client/ReactDOMTreeTraversal.js b/src/renderers/dom/client/ReactDOMTreeTraversal.js index c4767736b..4d809cfdb 100644 --- a/src/renderers/dom/client/ReactDOMTreeTraversal.js +++ b/src/renderers/dom/client/ReactDOMTreeTraversal.js @@ -20,7 +20,7 @@ var invariant = require('invariant'); function getLowestCommonAncestor(instA, instB) { - invariant('_nativeNode' in instA, 'getNodeFromInstance: Invalid argument.'); - invariant('_nativeNode' in instB, 'getNodeFromInstance: Invalid argument.'); + invariant('_hostNode' in instA, 'getNodeFromInstance: Invalid argument.'); + invariant('_hostNode' in instB, 'getNodeFromInstance: Invalid argument.'); var depthA = 0; - for (var tempA = instA; tempA; tempA = tempA._nativeParent) { + for (var tempA = instA; tempA; tempA = tempA._hostParent) { depthA++; @@ -28,3 +28,3 @@ function getLowestCommonAncestor(instA, instB) { var depthB = 0; - for (var tempB = instB; tempB; tempB = tempB._nativeParent) { + for (var tempB = instB; tempB; tempB = tempB._hostParent) { depthB++; @@ -34,3 +34,3 @@ function getLowestCommonAncestor(instA, instB) { while (depthA - depthB > 0) { - instA = instA._nativeParent; + instA = instA._hostParent; depthA--; @@ -40,3 +40,3 @@ function getLowestCommonAncestor(instA, instB) { while (depthB - depthA > 0) { - instB = instB._nativeParent; + instB = instB._hostParent; depthB--; @@ -50,4 +50,4 @@ function getLowestCommonAncestor(instA, instB) { } - instA = instA._nativeParent; - instB = instB._nativeParent; + instA = instA._hostParent; + instB = instB._hostParent; } @@ -60,4 +60,4 @@ function getLowestCommonAncestor(instA, instB) { function isAncestor(instA, instB) { - invariant('_nativeNode' in instA, 'isAncestor: Invalid argument.'); - invariant('_nativeNode' in instB, 'isAncestor: Invalid argument.'); + invariant('_hostNode' in instA, 'isAncestor: Invalid argument.'); + invariant('_hostNode' in instB, 'isAncestor: Invalid argument.'); @@ -67,3 +67,3 @@ function isAncestor(instA, instB) { } - instB = instB._nativeParent; + instB = instB._hostParent; } @@ -76,5 +76,5 @@ function isAncestor(instA, instB) { function getParentInstance(inst) { - invariant('_nativeNode' in inst, 'getParentInstance: Invalid argument.'); + invariant('_hostNode' in inst, 'getParentInstance: Invalid argument.'); - return inst._nativeParent; + return inst._hostParent; } @@ -88,10 +88,10 @@ function traverseTwoPhase(inst, fn, arg) { path.push(inst); - inst = inst._nativeParent; + inst = inst._hostParent; } var i; - for (i = path.length; i-- > 0;) { - fn(path[i], false, arg); + for (i = path.length; i-- > 0; ) { + fn(path[i], 'captured', arg); } for (i = 0; i < path.length; i++) { - fn(path[i], true, arg); + fn(path[i], 'bubbled', arg); } @@ -111,3 +111,3 @@ function traverseEnterLeave(from, to, fn, argFrom, argTo) { pathFrom.push(from); - from = from._nativeParent; + from = from._hostParent; } @@ -116,3 +116,3 @@ function traverseEnterLeave(from, to, fn, argFrom, argTo) { pathTo.push(to); - to = to._nativeParent; + to = to._hostParent; } @@ -120,6 +120,6 @@ function traverseEnterLeave(from, to, fn, argFrom, argTo) { for (i = 0; i < pathFrom.length; i++) { - fn(pathFrom[i], true, argFrom); + fn(pathFrom[i], 'bubbled', argFrom); } - for (i = pathTo.length; i-- > 0;) { - fn(pathTo[i], false, argTo); + for (i = pathTo.length; i-- > 0; ) { + fn(pathTo[i], 'captured', argTo); } diff --git a/src/renderers/dom/client/ReactEventListener.js b/src/renderers/dom/client/ReactEventListener.js index bbdbbcf29..a885cb9b9 100644 --- a/src/renderers/dom/client/ReactEventListener.js +++ b/src/renderers/dom/client/ReactEventListener.js @@ -31,4 +31,4 @@ function findParent(inst) { // mutation observer to listen for all DOM changes. - while (inst._nativeParent) { - inst = inst._nativeParent; + while (inst._hostParent) { + inst = inst._hostParent; } @@ -54,3 +54,3 @@ PooledClass.addPoolingTo( TopLevelCallbackBookKeeping, - PooledClass.twoArgumentPooler + PooledClass.twoArgumentPooler, ); @@ -60,3 +60,3 @@ function handleTopLevelImpl(bookKeeping) { var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode( - nativeEventTarget + nativeEventTarget, ); @@ -79,3 +79,3 @@ function handleTopLevelImpl(bookKeeping) { bookKeeping.nativeEvent, - getEventTarget(bookKeeping.nativeEvent) + getEventTarget(bookKeeping.nativeEvent), ); @@ -107,3 +107,2 @@ var ReactEventListener = { - /** @@ -113,3 +112,3 @@ var ReactEventListener = { * @param {string} handlerBaseName Event name (e.g. "click"). - * @param {object} handle Element on which to attach listener. + * @param {object} element Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully @@ -118,4 +117,3 @@ var ReactEventListener = { */ - trapBubbledEvent: function(topLevelType, handlerBaseName, handle) { - var element = handle; + trapBubbledEvent: function(topLevelType, handlerBaseName, element) { if (!element) { @@ -126,3 +124,3 @@ var ReactEventListener = { handlerBaseName, - ReactEventListener.dispatchEvent.bind(null, topLevelType) + ReactEventListener.dispatchEvent.bind(null, topLevelType), ); @@ -135,3 +133,3 @@ var ReactEventListener = { * @param {string} handlerBaseName Event name (e.g. "click"). - * @param {object} handle Element on which to attach listener. + * @param {object} element Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully @@ -140,4 +138,3 @@ var ReactEventListener = { */ - trapCapturedEvent: function(topLevelType, handlerBaseName, handle) { - var element = handle; + trapCapturedEvent: function(topLevelType, handlerBaseName, element) { if (!element) { @@ -148,3 +145,3 @@ var ReactEventListener = { handlerBaseName, - ReactEventListener.dispatchEvent.bind(null, topLevelType) + ReactEventListener.dispatchEvent.bind(null, topLevelType), ); @@ -164,3 +161,3 @@ var ReactEventListener = { topLevelType, - nativeEvent + nativeEvent, ); diff --git a/src/renderers/dom/client/ReactInputSelection.js b/src/renderers/dom/client/ReactInputSelection.js index 46e6931a6..ee9d33ddc 100644 --- a/src/renderers/dom/client/ReactInputSelection.js +++ b/src/renderers/dom/client/ReactInputSelection.js @@ -30,9 +30,9 @@ function isInDocument(node) { var ReactInputSelection = { - hasSelectionCapabilities: function(elem) { var nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase(); - return nodeName && ( - (nodeName === 'input' && elem.type === 'text') || - nodeName === 'textarea' || - elem.contentEditable === 'true' + return ( + nodeName && + ((nodeName === 'input' && elem.type === 'text') || + nodeName === 'textarea' || + elem.contentEditable === 'true') ); @@ -44,6 +44,5 @@ var ReactInputSelection = { focusedElem: focusedElem, - selectionRange: - ReactInputSelection.hasSelectionCapabilities(focusedElem) ? - ReactInputSelection.getSelection(focusedElem) : - null, + selectionRange: ReactInputSelection.hasSelectionCapabilities(focusedElem) + ? ReactInputSelection.getSelection(focusedElem) + : null, }; @@ -60,9 +59,5 @@ var ReactInputSelection = { var priorSelectionRange = priorSelectionInformation.selectionRange; - if (curFocusedElem !== priorFocusedElem && - isInDocument(priorFocusedElem)) { + if (curFocusedElem !== priorFocusedElem && isInDocument(priorFocusedElem)) { if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) { - ReactInputSelection.setSelection( - priorFocusedElem, - priorSelectionRange - ); + ReactInputSelection.setSelection(priorFocusedElem, priorSelectionRange); } @@ -87,4 +82,6 @@ var ReactInputSelection = { }; - } else if (document.selection && - (input.nodeName && input.nodeName.toLowerCase() === 'input')) { + } else if ( + document.selection && + (input.nodeName && input.nodeName.toLowerCase() === 'input') + ) { // IE8 input. @@ -123,4 +120,6 @@ var ReactInputSelection = { input.selectionEnd = Math.min(end, input.value.length); - } else if (document.selection && - (input.nodeName && input.nodeName.toLowerCase() === 'input')) { + } else if ( + document.selection && + (input.nodeName && input.nodeName.toLowerCase() === 'input') + ) { var range = input.createTextRange(); diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 7f6f8cac6..382aff890 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -15,2 +15,3 @@ var DOMLazyTree = require('DOMLazyTree'); var DOMProperty = require('DOMProperty'); +var React = require('React'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); @@ -20,7 +21,6 @@ var ReactDOMContainerInfo = require('ReactDOMContainerInfo'); var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); -var ReactElement = require('ReactElement'); var ReactFeatureFlags = require('ReactFeatureFlags'); +var ReactInstanceMap = require('ReactInstanceMap'); var ReactInstrumentation = require('ReactInstrumentation'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); -var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); @@ -82,3 +82,3 @@ function internalGetID(node) { // the empty string, as if the attribute were missing. - return node.getAttribute && node.getAttribute(ATTR_NAME) || ''; + return (node.getAttribute && node.getAttribute(ATTR_NAME)) || ''; } @@ -98,3 +98,3 @@ function mountComponentIntoNode( shouldReuseMarkup, - context + context, ) { @@ -102,8 +102,7 @@ function mountComponentIntoNode( if (ReactFeatureFlags.logTopLevelRenders) { - var wrappedElement = wrapperInstance._currentElement.props; + var wrappedElement = wrapperInstance._currentElement.props.child; var type = wrappedElement.type; - markerName = 'React mount: ' + ( - typeof type === 'string' ? type : - type.displayName || type.name - ); + markerName = + 'React mount: ' + + (typeof type === 'string' ? type : type.displayName || type.name); console.time(markerName); @@ -116,3 +115,4 @@ function mountComponentIntoNode( ReactDOMContainerInfo(wrapperInstance, container), - context + context, + 0 /* parentDebugID */, ); @@ -129,3 +129,3 @@ function mountComponentIntoNode( shouldReuseMarkup, - transaction + transaction, ); @@ -144,3 +144,3 @@ function batchedMountComponentIntoNode( shouldReuseMarkup, - context + context, ) { @@ -148,3 +148,3 @@ function batchedMountComponentIntoNode( /* useCreateElement */ - !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement + !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement, ); @@ -157,3 +157,3 @@ function batchedMountComponentIntoNode( shouldReuseMarkup, - context + context, ); @@ -172,3 +172,9 @@ function batchedMountComponentIntoNode( function unmountComponentFromNode(instance, container, safely) { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } ReactReconciler.unmountComponent(instance, safely); + if (__DEV__) { + ReactInstrumentation.debugTool.onEndFlush(); + } @@ -198,3 +204,3 @@ function hasNonRootReactChild(container) { var inst = ReactDOMComponentTree.getInstanceFromNode(rootEl); - return !!(inst && inst._nativeParent); + return !!(inst && inst._hostParent); } @@ -202,9 +208,46 @@ function hasNonRootReactChild(container) { -function getNativeRootInstanceInContainer(container) { +/** + * True if the supplied DOM node is a React DOM element and + * it has been rendered by another copy of React. + * + * @param {?DOMElement} node The candidate DOM node. + * @return {boolean} True if the DOM has been rendered by another copy of React + * @internal + */ +function nodeIsRenderedByOtherInstance(container) { var rootEl = getReactRootElementInContainer(container); - var prevNativeInstance = - rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl); + return !!( + rootEl && + isReactNode(rootEl) && + !ReactDOMComponentTree.getInstanceFromNode(rootEl) + ); +} + +/** + * True if the supplied DOM node is a valid node element. + * + * @param {?DOMElement} node The candidate DOM node. + * @return {boolean} True if the DOM is a valid DOM node. + * @internal + */ +function isValidContainer(node) { + return !!( + node && + (node.nodeType === ELEMENT_NODE_TYPE || + node.nodeType === DOC_NODE_TYPE || + node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) + ); +} + +/** + * True if the supplied DOM node is a valid React node element. + * + * @param {?DOMElement} node The candidate DOM node. + * @return {boolean} True if the DOM is a valid React DOM node. + * @internal + */ +function isReactNode(node) { return ( - prevNativeInstance && !prevNativeInstance._nativeParent ? - prevNativeInstance : null + isValidContainer(node) && + (node.hasAttribute(ROOT_ATTR_NAME) || node.hasAttribute(ATTR_NAME)) ); @@ -212,5 +255,14 @@ function getNativeRootInstanceInContainer(container) { +function getHostRootInstanceInContainer(container) { + var rootEl = getReactRootElementInContainer(container); + var prevHostInstance = + rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl); + return prevHostInstance && !prevHostInstance._hostParent + ? prevHostInstance + : null; +} + function getTopLevelWrapperInContainer(container) { - var root = getNativeRootInstanceInContainer(container); - return root ? root._nativeContainerInfo._topLevelWrapper : null; + var root = getHostRootInstanceInContainer(container); + return root ? root._hostContainerInfo._topLevelWrapper : null; } @@ -231,5 +283,5 @@ if (__DEV__) { TopLevelWrapper.prototype.render = function() { - // this.props is actually a ReactElement - return this.props; + return this.props.child; }; +TopLevelWrapper.isReactTopLevelWrapper = true; @@ -254,3 +306,2 @@ TopLevelWrapper.prototype.render = function() { var ReactMount = { - TopLevelWrapper: TopLevelWrapper, @@ -282,8 +333,14 @@ var ReactMount = { _updateRootComponent: function( - prevComponent, - nextElement, - container, - callback) { + prevComponent, + nextElement, + nextContext, + container, + callback, + ) { ReactMount.scrollMonitor(container, function() { - ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement); + ReactUpdateQueue.enqueueElementInternal( + prevComponent, + nextElement, + nextContext, + ); if (callback) { @@ -297,3 +354,3 @@ var ReactMount = { /** - * Render a new component into the DOM. Hooked by devtools! + * Render a new component into the DOM. Hooked by hooks! * @@ -308,3 +365,3 @@ var ReactMount = { shouldReuseMarkup, - context + context, ) { @@ -316,7 +373,7 @@ var ReactMount = { '_renderNewRootComponent(): Render methods should be a pure function ' + - 'of props and state; triggering nested component updates from ' + - 'render is not allowed. If necessary, trigger nested updates in ' + - 'componentDidUpdate. Check the render method of %s.', - ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || - 'ReactCompositeComponent' + 'of props and state; triggering nested component updates from ' + + 'render is not allowed. If necessary, trigger nested updates in ' + + 'componentDidUpdate. Check the render method of %s.', + (ReactCurrentOwner.current && ReactCurrentOwner.current.getName()) || + 'ReactCompositeComponent', ); @@ -324,8 +381,4 @@ var ReactMount = { invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE || - container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE - ), - '_registerComponent(...): Target container is not a DOM element.' + isValidContainer(container), + '_registerComponent(...): Target container is not a DOM element.', ); @@ -333,3 +386,3 @@ var ReactMount = { ReactBrowserEventEmitter.ensureScrollValueMonitoring(); - var componentInstance = instantiateReactComponent(nextElement); + var componentInstance = instantiateReactComponent(nextElement, false); @@ -344,3 +397,3 @@ var ReactMount = { shouldReuseMarkup, - context + context, ); @@ -350,6 +403,2 @@ var ReactMount = { - if (__DEV__) { - ReactInstrumentation.debugTool.onMountRootComponent(componentInstance); - } - return componentInstance; @@ -370,6 +419,11 @@ var ReactMount = { */ - renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) { + renderSubtreeIntoContainer: function( + parentComponent, + nextElement, + container, + callback, + ) { invariant( - parentComponent != null && parentComponent._reactInternalInstance != null, - 'parentComponent must be a valid React Component' + parentComponent != null && ReactInstanceMap.has(parentComponent), + 'parentComponent must be a valid React Component', ); @@ -379,3 +433,3 @@ var ReactMount = { container, - callback + callback, ); @@ -383,20 +437,23 @@ var ReactMount = { - _renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) { + _renderSubtreeIntoContainer: function( + parentComponent, + nextElement, + container, + callback, + ) { ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render'); invariant( - ReactElement.isValidElement(nextElement), + React.isValidElement(nextElement), 'ReactDOM.render(): Invalid component element.%s', - ( - typeof nextElement === 'string' ? - ' Instead of passing a string like \'div\', pass ' + - 'React.createElement(\'div\') or
.' : - typeof nextElement === 'function' ? - ' Instead of passing a class like Foo, pass ' + - 'React.createElement(Foo) or .' : - // Check if it quacks like an element - nextElement != null && nextElement.props !== undefined ? - ' This may be caused by unintentionally loading two independent ' + - 'copies of React.' : - '' - ) + typeof nextElement === 'string' + ? " Instead of passing a string like 'div', pass " + + "React.createElement('div') or
." + : typeof nextElement === 'function' + ? ' Instead of passing a class like Foo, pass ' + + 'React.createElement(Foo) or .' + : // Check if it quacks like an element + nextElement != null && nextElement.props !== undefined + ? ' This may be caused by unintentionally loading two independent ' + + 'copies of React.' + : '', ); @@ -404,20 +461,23 @@ var ReactMount = { warning( - !container || !container.tagName || - container.tagName.toUpperCase() !== 'BODY', + !container || + !container.tagName || + container.tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + - 'discouraged, since its children are often manipulated by third-party ' + - 'scripts and browser extensions. This may lead to subtle ' + - 'reconciliation issues. Try rendering into a container element created ' + - 'for your app.' + 'discouraged, since its children are often manipulated by third-party ' + + 'scripts and browser extensions. This may lead to subtle ' + + 'reconciliation issues. Try rendering into a container element created ' + + 'for your app.', ); - var nextWrappedElement = ReactElement( - TopLevelWrapper, - null, - null, - null, - null, - null, - nextElement - ); + var nextWrappedElement = React.createElement(TopLevelWrapper, { + child: nextElement, + }); + + var nextContext; + if (parentComponent) { + var parentInst = ReactInstanceMap.get(parentComponent); + nextContext = parentInst._processChildContext(parentInst._context); + } else { + nextContext = emptyObject; + } @@ -427,8 +487,10 @@ var ReactMount = { var prevWrappedElement = prevComponent._currentElement; - var prevElement = prevWrappedElement.props; + var prevElement = prevWrappedElement.props.child; if (shouldUpdateReactComponent(prevElement, nextElement)) { var publicInst = prevComponent._renderedComponent.getPublicInstance(); - var updatedCallback = callback && function() { - callback.call(publicInst); - }; + var updatedCallback = + callback && + function() { + callback.call(publicInst); + }; ReactMount._updateRootComponent( @@ -436,4 +498,5 @@ var ReactMount = { nextWrappedElement, + nextContext, container, - updatedCallback + updatedCallback, ); @@ -454,5 +517,5 @@ var ReactMount = { 'render(...): Replacing React-rendered children with a new root ' + - 'component. If you intended to update the children of this node, ' + - 'you should instead have the existing children update their state ' + - 'and render the new components instead of calling ReactDOM.render.' + 'component. If you intended to update the children of this node, ' + + 'you should instead have the existing children update their state ' + + 'and render the new components instead of calling ReactDOM.render.', ); @@ -466,4 +529,4 @@ var ReactMount = { 'render(): Target node has markup rendered by React, but there ' + - 'are unrelated nodes as well. This is most commonly caused by ' + - 'white-space inserted around server-rendered markup.' + 'are unrelated nodes as well. This is most commonly caused by ' + + 'white-space inserted around server-rendered markup.', ); @@ -484,7 +547,3 @@ var ReactMount = { shouldReuseMarkup, - parentComponent != null ? - parentComponent._reactInternalInstance._processChildContext( - parentComponent._reactInternalInstance._context - ) : - emptyObject + nextContext, )._renderedComponent.getPublicInstance(); @@ -496,5 +555,5 @@ var ReactMount = { - /** * Renders a React component into the DOM in the supplied `container`. + * See https://facebook.github.io/react/docs/top-level-api.html#reactdom.render * @@ -510,3 +569,8 @@ var ReactMount = { render: function(nextElement, container, callback) { - return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback); + return ReactMount._renderSubtreeIntoContainer( + null, + nextElement, + container, + callback, + ); }, @@ -515,2 +579,3 @@ var ReactMount = { * Unmounts and destroys the React component rendered in the `container`. + * See https://facebook.github.io/react/docs/top-level-api.html#reactdom.unmountcomponentatnode * @@ -528,7 +593,7 @@ var ReactMount = { 'unmountComponentAtNode(): Render methods should be a pure function ' + - 'of props and state; triggering nested component updates from render ' + - 'is not allowed. If necessary, trigger nested updates in ' + - 'componentDidUpdate. Check the render method of %s.', - ReactCurrentOwner.current && ReactCurrentOwner.current.getName() || - 'ReactCompositeComponent' + 'of props and state; triggering nested component updates from render ' + + 'is not allowed. If necessary, trigger nested updates in ' + + 'componentDidUpdate. Check the render method of %s.', + (ReactCurrentOwner.current && ReactCurrentOwner.current.getName()) || + 'ReactCompositeComponent', ); @@ -536,10 +601,14 @@ var ReactMount = { invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE || - container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE - ), - 'unmountComponentAtNode(...): Target container is not a DOM element.' + isValidContainer(container), + 'unmountComponentAtNode(...): Target container is not a DOM element.', ); + if (__DEV__) { + warning( + !nodeIsRenderedByOtherInstance(container), + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); + } + var prevComponent = getTopLevelWrapperInContainer(container); @@ -557,11 +626,9 @@ var ReactMount = { !containerHasNonRootReactChild, - 'unmountComponentAtNode(): The node you\'re attempting to unmount ' + - 'was rendered by React and is not a top-level container. %s', - ( - isContainerReactRoot ? - 'You may have accidentally passed in a React root node instead ' + - 'of its container.' : - 'Instead, have the parent component update its state and ' + - 'rerender in order to remove this component.' - ) + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by React and is not a top-level container. %s', + isContainerReactRoot + ? 'You may have accidentally passed in a React root node instead ' + + 'of its container.' + : 'Instead, have the parent component update its state and ' + + 'rerender in order to remove this component.', ); @@ -576,3 +643,3 @@ var ReactMount = { container, - false + false, ); @@ -586,11 +653,7 @@ var ReactMount = { shouldReuseMarkup, - transaction + transaction, ) { invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE || - container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE - ), - 'mountComponentIntoNode(...): Target container is not valid.' + isValidContainer(container), + 'mountComponentIntoNode(...): Target container is not valid.', ); @@ -604,3 +667,3 @@ var ReactMount = { var checksum = rootElement.getAttribute( - ReactMarkupChecksum.CHECKSUM_ATTR_NAME + ReactMarkupChecksum.CHECKSUM_ATTR_NAME, ); @@ -611,3 +674,3 @@ var ReactMount = { ReactMarkupChecksum.CHECKSUM_ATTR_NAME, - checksum + checksum, ); @@ -629,3 +692,4 @@ var ReactMount = { normalizer.contentDocument.write(markup); - normalizedMarkup = normalizer.contentDocument.documentElement.outerHTML; + normalizedMarkup = + normalizer.contentDocument.documentElement.outerHTML; document.body.removeChild(normalizer); @@ -635,5 +699,7 @@ var ReactMount = { var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup); - var difference = ' (client) ' + + var difference = + ' (client) ' + normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) + - '\n (server) ' + rootMarkup.substring(diffIndex - 20, diffIndex + 20); + '\n (server) ' + + rootMarkup.substring(diffIndex - 20, diffIndex + 20); @@ -641,11 +707,11 @@ var ReactMount = { container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document using ' + - 'server rendering but the checksum was invalid. This usually ' + - 'means you rendered a different component type or props on ' + - 'the client from the one on the server, or your render() ' + - 'methods are impure. React cannot handle this case due to ' + - 'cross-browser quirks by rendering at the document root. You ' + - 'should look for environment dependent code in your components ' + - 'and ensure the props are the same client and server side:\n%s', - difference + "You're trying to render a component to the document using " + + 'server rendering but the checksum was invalid. This usually ' + + 'means you rendered a different component type or props on ' + + 'the client from the one on the server, or your render() ' + + 'methods are impure. React cannot handle this case due to ' + + 'cross-browser quirks by rendering at the document root. You ' + + 'should look for environment dependent code in your components ' + + 'and ensure the props are the same client and server side:\n%s', + difference, ); @@ -656,10 +722,10 @@ var ReactMount = { 'React attempted to reuse markup in a container but the ' + - 'checksum was invalid. This generally means that you are ' + - 'using server rendering and the markup generated on the ' + - 'server was not what the client was expecting. React injected ' + - 'new markup to compensate which works but you have lost many ' + - 'of the benefits of server rendering. Instead, figure out ' + - 'why the markup being generated is different on the client ' + - 'or server:\n%s', - difference + 'checksum was invalid. This generally means that you are ' + + 'using server rendering and the markup generated on the ' + + 'server was not what the client was expecting. React injected ' + + 'new markup to compensate which works but you have lost many ' + + 'of the benefits of server rendering. Instead, figure out ' + + 'why the markup being generated is different on the client ' + + 'or server:\n%s', + difference, ); @@ -671,6 +737,6 @@ var ReactMount = { container.nodeType !== DOC_NODE_TYPE, - 'You\'re trying to render a component to the document but ' + - 'you didn\'t use server rendering. We can\'t do this ' + + "You're trying to render a component to the document but " + + "you didn't use server rendering. We can't do this " + 'without using server rendering due to cross-browser quirks. ' + - 'See ReactDOMServer.renderToString() for server rendering.' + 'See ReactDOMServer.renderToString() for server rendering.', ); @@ -686,2 +752,15 @@ var ReactMount = { } + + if (__DEV__) { + var hostNode = ReactDOMComponentTree.getInstanceFromNode( + container.firstChild, + ); + if (hostNode._debugID !== 0) { + ReactInstrumentation.debugTool.onHostOperation({ + instanceID: hostNode._debugID, + type: 'mount', + payload: markup.toString(), + }); + } + } }, @@ -689,7 +768,2 @@ var ReactMount = { -ReactPerf.measureMethods(ReactMount, 'ReactMount', { - _renderNewRootComponent: '_renderNewRootComponent', - _mountImageIntoNode: '_mountImageIntoNode', -}); - module.exports = ReactMount; diff --git a/src/renderers/dom/client/ReactReconcileTransaction.js b/src/renderers/dom/client/ReactReconcileTransaction.js index fedd883bf..4b3950bbf 100644 --- a/src/renderers/dom/client/ReactReconcileTransaction.js +++ b/src/renderers/dom/client/ReactReconcileTransaction.js @@ -17,4 +17,5 @@ var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactInputSelection = require('ReactInputSelection'); +var ReactInstrumentation = require('ReactInstrumentation'); var Transaction = require('Transaction'); - +var ReactUpdateQueue = require('ReactUpdateQueue'); @@ -92,2 +93,9 @@ var TRANSACTION_WRAPPERS = [ +if (__DEV__) { + TRANSACTION_WRAPPERS.push({ + initialize: ReactInstrumentation.debugTool.onBeginFlush, + close: ReactInstrumentation.debugTool.onEndFlush, + }); +} + /** @@ -106,3 +114,3 @@ var TRANSACTION_WRAPPERS = [ */ -function ReactReconcileTransaction(useCreateElement) { +function ReactReconcileTransaction(useCreateElement: boolean) { this.reinitializeTransaction(); @@ -112,3 +120,3 @@ function ReactReconcileTransaction(useCreateElement) { // accessible and defaults to false when `ReactDOMComponent` and - // `ReactTextComponent` checks it in `mountComponent`.` + // `ReactDOMTextComponent` checks it in `mountComponent`.` this.renderToStaticMarkup = false; @@ -137,2 +145,9 @@ var Mixin = { + /** + * @return {object} The queue to collect React async events. + */ + getUpdateQueue: function() { + return ReactUpdateQueue; + }, + /** @@ -160,4 +175,3 @@ var Mixin = { - -Object.assign(ReactReconcileTransaction.prototype, Transaction.Mixin, Mixin); +Object.assign(ReactReconcileTransaction.prototype, Transaction, Mixin); diff --git a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js index 5dbf91c2c..fb0f0619b 100644 --- a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js +++ b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js @@ -13,4 +13,2 @@ -var keyOf = require('keyOf'); - var EventListener; @@ -37,7 +35,7 @@ var recordIDAndReturnFalse = function(id, event) { }; -var LISTENER = jest.genMockFn(); -var ON_CLICK_KEY = keyOf({onClick: null}); -var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); -var ON_CHANGE_KEY = keyOf({onChange: null}); -var ON_MOUSE_ENTER_KEY = keyOf({onMouseEnter: null}); +var LISTENER = jest.fn(); +var ON_CLICK_KEY = 'onClick'; +var ON_TOUCH_TAP_KEY = 'onTouchTap'; +var ON_CHANGE_KEY = 'onChange'; +var ON_MOUSE_ENTER_KEY = 'onMouseEnter'; @@ -58,5 +56,4 @@ function getInternal(node) { - -describe('ReactBrowserEventEmitter', function() { - beforeEach(function() { +describe('ReactBrowserEventEmitter', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -73,7 +70,7 @@ describe('ReactBrowserEventEmitter', function() { ReactTestUtils.renderIntoDocument( -
GRANDPARENT = c}> -
PARENT = c}> -
CHILD = c} /> +
(GRANDPARENT = c)}> +
(PARENT = c)}> +
(CHILD = c)} />
-
+
, ); @@ -87,3 +84,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should store a listener correctly', function() { + it('should store a listener correctly', () => { registerSimpleTestHandler(); @@ -93,3 +90,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should retrieve a listener correctly', function() { + it('should retrieve a listener correctly', () => { registerSimpleTestHandler(); @@ -99,3 +96,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should clear all handlers when asked to', function() { + it('should clear all handlers when asked to', () => { registerSimpleTestHandler(); @@ -106,3 +103,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should invoke a simple handler registered on a node', function() { + it('should invoke a simple handler registered on a node', () => { registerSimpleTestHandler(); @@ -112,16 +109,13 @@ describe('ReactBrowserEventEmitter', function() { - it( - 'should not invoke handlers if ReactBrowserEventEmitter is disabled', - function() { - registerSimpleTestHandler(); - ReactBrowserEventEmitter.setEnabled(false); - ReactTestUtils.SimulateNative.click(CHILD); - expect(LISTENER.mock.calls.length).toBe(0); - ReactBrowserEventEmitter.setEnabled(true); - ReactTestUtils.SimulateNative.click(CHILD); - expect(LISTENER.mock.calls.length).toBe(1); - } - ); + it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => { + registerSimpleTestHandler(); + ReactBrowserEventEmitter.setEnabled(false); + ReactTestUtils.SimulateNative.click(CHILD); + expect(LISTENER.mock.calls.length).toBe(0); + ReactBrowserEventEmitter.setEnabled(true); + ReactTestUtils.SimulateNative.click(CHILD); + expect(LISTENER.mock.calls.length).toBe(1); + }); - it('should bubble simply', function() { + it('should bubble simply', () => { EventPluginHub.putListener( @@ -129,3 +123,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -134,3 +128,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(PARENT)) + recordID.bind(null, getInternal(PARENT)), ); @@ -139,3 +133,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -148,3 +142,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should continue bubbling if an error is thrown', function() { + it('should continue bubbling if an error is thrown', () => { EventPluginHub.putListener( @@ -152,12 +146,8 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(CHILD)) - ); - EventPluginHub.putListener( - getInternal(PARENT), - ON_CLICK_KEY, - function() { - recordID(getInternal(PARENT)); - throw new Error('Handler interrupted'); - } + recordID.bind(null, getInternal(CHILD)), ); + EventPluginHub.putListener(getInternal(PARENT), ON_CLICK_KEY, function() { + recordID(getInternal(PARENT)); + throw new Error('Handler interrupted'); + }); EventPluginHub.putListener( @@ -165,3 +155,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -176,27 +166,21 @@ describe('ReactBrowserEventEmitter', function() { - it('should set currentTarget', function() { - EventPluginHub.putListener( - getInternal(CHILD), - ON_CLICK_KEY, - function(event) { - recordID(getInternal(CHILD)); - expect(event.currentTarget).toBe(CHILD); - } - ); - EventPluginHub.putListener( - getInternal(PARENT), - ON_CLICK_KEY, - function(event) { - recordID(getInternal(PARENT)); - expect(event.currentTarget).toBe(PARENT); - } - ); - EventPluginHub.putListener( - getInternal(GRANDPARENT), - ON_CLICK_KEY, - function(event) { - recordID(getInternal(GRANDPARENT)); - expect(event.currentTarget).toBe(GRANDPARENT); - } - ); + it('should set currentTarget', () => { + EventPluginHub.putListener(getInternal(CHILD), ON_CLICK_KEY, function( + event, + ) { + recordID(getInternal(CHILD)); + expect(event.currentTarget).toBe(CHILD); + }); + EventPluginHub.putListener(getInternal(PARENT), ON_CLICK_KEY, function( + event, + ) { + recordID(getInternal(PARENT)); + expect(event.currentTarget).toBe(PARENT); + }); + EventPluginHub.putListener(getInternal(GRANDPARENT), ON_CLICK_KEY, function( + event, + ) { + recordID(getInternal(GRANDPARENT)); + expect(event.currentTarget).toBe(GRANDPARENT); + }); ReactTestUtils.Simulate.click(CHILD); @@ -208,3 +192,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should support stopPropagation()', function() { + it('should support stopPropagation()', () => { EventPluginHub.putListener( @@ -212,3 +196,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -217,3 +201,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, getInternal(PARENT)) + recordIDAndStopPropagation.bind(null, getInternal(PARENT)), ); @@ -222,3 +206,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -230,3 +214,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should stop after first dispatch if stopPropagation', function() { + it('should stop after first dispatch if stopPropagation', () => { EventPluginHub.putListener( @@ -234,3 +218,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordIDAndStopPropagation.bind(null, getInternal(CHILD)) + recordIDAndStopPropagation.bind(null, getInternal(CHILD)), ); @@ -239,3 +223,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(PARENT)) + recordID.bind(null, getInternal(PARENT)), ); @@ -244,3 +228,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -251,3 +235,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should not stopPropagation if false is returned', function() { + it('should not stopPropagation if false is returned', () => { EventPluginHub.putListener( @@ -255,3 +239,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordIDAndReturnFalse.bind(null, getInternal(CHILD)) + recordIDAndReturnFalse.bind(null, getInternal(CHILD)), ); @@ -260,3 +244,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(PARENT)) + recordID.bind(null, getInternal(PARENT)), ); @@ -265,3 +249,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -273,3 +257,3 @@ describe('ReactBrowserEventEmitter', function() { expect(idCallOrder[2]).toBe(getInternal(GRANDPARENT)); - expect(console.error.calls.length).toEqual(0); + expect(console.error.calls.count()).toEqual(0); }); @@ -285,4 +269,4 @@ describe('ReactBrowserEventEmitter', function() { - it('should invoke handlers that were removed while bubbling', function() { - var handleParentClick = jest.genMockFn(); + it('should invoke handlers that were removed while bubbling', () => { + var handleParentClick = jest.fn(); var handleChildClick = function(event) { @@ -293,3 +277,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - handleChildClick + handleChildClick, ); @@ -298,3 +282,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - handleParentClick + handleParentClick, ); @@ -304,4 +288,4 @@ describe('ReactBrowserEventEmitter', function() { - it('should not invoke newly inserted handlers while bubbling', function() { - var handleParentClick = jest.genMockFn(); + it('should not invoke newly inserted handlers while bubbling', () => { + var handleParentClick = jest.fn(); var handleChildClick = function(event) { @@ -310,3 +294,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - handleParentClick + handleParentClick, ); @@ -316,3 +300,3 @@ describe('ReactBrowserEventEmitter', function() { ON_CLICK_KEY, - handleChildClick + handleChildClick, ); @@ -322,3 +306,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should have mouse enter simulated by test utils', function() { + it('should have mouse enter simulated by test utils', () => { EventPluginHub.putListener( @@ -326,3 +310,3 @@ describe('ReactBrowserEventEmitter', function() { ON_MOUSE_ENTER_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -333,3 +317,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should infer onTouchTap from a touchStart/End', function() { + it('should infer onTouchTap from a touchStart/End', () => { EventPluginHub.putListener( @@ -337,3 +321,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -341,3 +325,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -345,3 +329,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -351,3 +335,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should infer onTouchTap from when dragging below threshold', function() { + it('should infer onTouchTap from when dragging below threshold', () => { EventPluginHub.putListener( @@ -355,3 +339,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -359,3 +343,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -363,3 +347,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1) + ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1), ); @@ -369,3 +353,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should not onTouchTap from when dragging beyond threshold', function() { + it('should not onTouchTap from when dragging beyond threshold', () => { EventPluginHub.putListener( @@ -373,3 +357,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -377,3 +361,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -381,3 +365,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, tapMoveThreshold + 1) + ReactTestUtils.nativeTouchData(0, tapMoveThreshold + 1), ); @@ -386,3 +370,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should listen to events only once', function() { + it('should listen to events only once', () => { spyOn(EventListener, 'listen'); @@ -390,6 +374,6 @@ describe('ReactBrowserEventEmitter', function() { ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - expect(EventListener.listen.calls.length).toBe(1); + expect(EventListener.listen.calls.count()).toBe(1); }); - it('should work with event plugins without dependencies', function() { + it('should work with event plugins without dependencies', () => { spyOn(EventListener, 'listen'); @@ -398,6 +382,6 @@ describe('ReactBrowserEventEmitter', function() { - expect(EventListener.listen.argsForCall[0][1]).toBe('click'); + expect(EventListener.listen.calls.argsFor(0)[1]).toBe('click'); }); - it('should work with event plugins with dependencies', function() { + it('should work with event plugins with dependencies', () => { spyOn(EventListener, 'listen'); @@ -408,4 +392,4 @@ describe('ReactBrowserEventEmitter', function() { var setEventListeners = []; - var listenCalls = EventListener.listen.argsForCall; - var captureCalls = EventListener.capture.argsForCall; + var listenCalls = EventListener.listen.calls.allArgs(); + var captureCalls = EventListener.capture.calls.allArgs(); for (var i = 0; i < listenCalls.length; i++) { @@ -426,3 +410,3 @@ describe('ReactBrowserEventEmitter', function() { - it('should bubble onTouchTap', function() { + it('should bubble onTouchTap', () => { EventPluginHub.putListener( @@ -430,3 +414,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(CHILD)) + recordID.bind(null, getInternal(CHILD)), ); @@ -435,3 +419,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(PARENT)) + recordID.bind(null, getInternal(PARENT)), ); @@ -440,3 +424,3 @@ describe('ReactBrowserEventEmitter', function() { ON_TOUCH_TAP_KEY, - recordID.bind(null, getInternal(GRANDPARENT)) + recordID.bind(null, getInternal(GRANDPARENT)), ); @@ -444,3 +428,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -448,3 +432,3 @@ describe('ReactBrowserEventEmitter', function() { CHILD, - ReactTestUtils.nativeTouchData(0, 0) + ReactTestUtils.nativeTouchData(0, 0), ); @@ -456,2 +440,17 @@ describe('ReactBrowserEventEmitter', function() { + it('should not crash ensureScrollValueMonitoring when createEvent returns null', () => { + var originalCreateEvent = document.createEvent; + document.createEvent = function() { + return null; + }; + spyOn(document, 'createEvent'); + + try { + var hasEventPageXY = ReactBrowserEventEmitter.supportsEventPageXY(); + expect(document.createEvent.calls.count()).toBe(1); + expect(hasEventPageXY).toBe(false); + } finally { + document.createEvent = originalCreateEvent; + } + }); }); diff --git a/src/renderers/dom/client/__tests__/ReactDOM-test.js b/src/renderers/dom/client/__tests__/ReactDOM-test.js index 9c3cef57f..b55e913ff 100644 --- a/src/renderers/dom/client/__tests__/ReactDOM-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOM-test.js @@ -18,3 +18,3 @@ var div = React.createFactory('div'); -describe('ReactDOM', function() { +describe('ReactDOM', () => { // TODO: uncomment this test once we can run in phantom, which @@ -48,3 +48,3 @@ describe('ReactDOM', function() { - it('allows a DOM element to be used with a string', function() { + it('allows a DOM element to be used with a string', () => { var element = React.createElement('div', {className: 'foo'}); @@ -54,6 +54,4 @@ describe('ReactDOM', function() { - it('should allow children to be passed as an argument', function() { - var argDiv = ReactTestUtils.renderIntoDocument( - div(null, 'child') - ); + it('should allow children to be passed as an argument', () => { + var argDiv = ReactTestUtils.renderIntoDocument(div(null, 'child')); var argNode = ReactDOM.findDOMNode(argDiv); @@ -62,5 +60,5 @@ describe('ReactDOM', function() { - it('should overwrite props.children with children argument', function() { + it('should overwrite props.children with children argument', () => { var conflictDiv = ReactTestUtils.renderIntoDocument( - div({children: 'fakechild'}, 'child') + div({children: 'fakechild'}, 'child'), ); @@ -74,3 +72,3 @@ describe('ReactDOM', function() { */ - it('should purge the DOM cache when removing nodes', function() { + it('should purge the DOM cache when removing nodes', () => { var myDiv = ReactTestUtils.renderIntoDocument( @@ -79,3 +77,3 @@ describe('ReactDOM', function() {
-
+
, ); @@ -86,3 +84,3 @@ describe('ReactDOM', function() {
, -
+
, ); @@ -92,3 +90,3 @@ describe('ReactDOM', function() {
, -
+
, ); @@ -99,3 +97,3 @@ describe('ReactDOM', function() {
, -
+
, ); @@ -106,3 +104,3 @@ describe('ReactDOM', function() {
, -
+
, ); @@ -113,10 +111,10 @@ describe('ReactDOM', function() { - it('allow React.DOM factories to be called without warnings', function() { - spyOn(console, 'error'); + it('throws warning when React.DOM factories are called', () => { + spyOn(console, 'warn'); var element = React.DOM.div(); expect(element.type).toBe('div'); - expect(console.error.argsForCall.length).toBe(0); + expect(console.warn.calls.count()).toBe(1); }); - it('throws in render() if the mount callback is not a function', function() { + it('throws in render() if the mount callback is not a function', () => { function Foo() { @@ -125,23 +123,23 @@ describe('ReactDOM', function() { } - var A = React.createClass({ - getInitialState: function() { - return {}; - }, - render: function() { + + class A extends React.Component { + state = {}; + + render() { return
; - }, - }); + } + } var myDiv = document.createElement('div'); - expect(() => ReactDOM.render(, myDiv, 'no')).toThrow( + expect(() => ReactDOM.render(, myDiv, 'no')).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: string.' + 'to be a function. Instead received: string.', ); - expect(() => ReactDOM.render(, myDiv, {})).toThrow( + expect(() => ReactDOM.render(, myDiv, {})).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: Object.' + 'to be a function. Instead received: Object.', ); - expect(() => ReactDOM.render(, myDiv, new Foo())).toThrow( + expect(() => ReactDOM.render(, myDiv, new Foo())).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: Foo (keys: a, b).' + 'to be a function. Instead received: Foo (keys: a, b).', ); @@ -149,3 +147,3 @@ describe('ReactDOM', function() { - it('throws in render() if the update callback is not a function', function() { + it('throws in render() if the update callback is not a function', () => { function Foo() { @@ -154,10 +152,10 @@ describe('ReactDOM', function() { } - var A = React.createClass({ - getInitialState: function() { - return {}; - }, - render: function() { + + class A extends React.Component { + state = {}; + + render() { return
; - }, - }); + } + } @@ -166,13 +164,13 @@ describe('ReactDOM', function() { - expect(() => ReactDOM.render(, myDiv, 'no')).toThrow( + expect(() => ReactDOM.render(, myDiv, 'no')).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: string.' + 'to be a function. Instead received: string.', ); - expect(() => ReactDOM.render(, myDiv, {})).toThrow( + expect(() => ReactDOM.render(, myDiv, {})).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: Object.' + 'to be a function. Instead received: Object.', ); - expect(() => ReactDOM.render(, myDiv, new Foo())).toThrow( + expect(() => ReactDOM.render(, myDiv, new Foo())).toThrowError( 'ReactDOM.render(...): Expected the last optional `callback` argument ' + - 'to be a function. Instead received: Foo (keys: a, b).' + 'to be a function. Instead received: Foo (keys: a, b).', ); diff --git a/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js index 89ec35d58..f6ce6f428 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js @@ -13,3 +13,3 @@ -describe('ReactDOMComponentTree', function() { +describe('ReactDOMComponentTree', () => { var React; @@ -26,3 +26,3 @@ describe('ReactDOMComponentTree', function() { - beforeEach(function() { + beforeEach(() => { React = require('React'); @@ -33,3 +33,3 @@ describe('ReactDOMComponentTree', function() { - it('finds nodes for instances', function() { + it('finds nodes for instances', () => { // This is a little hard to test directly. But refs rely on it -- so we @@ -37,4 +37,4 @@ describe('ReactDOMComponentTree', function() { // other nodes don't have a ref. - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { var toRef = this.props.toRef; @@ -49,4 +49,4 @@ describe('ReactDOMComponentTree', function() { ); - }, - }); + } + } @@ -63,5 +63,5 @@ describe('ReactDOMComponentTree', function() { - it('finds instances for nodes', function() { - var Component = React.createClass({ - render: function() { + it('finds instances for nodes', () => { + class Component extends React.Component { + render() { return ( @@ -76,4 +76,4 @@ describe('ReactDOMComponentTree', function() { ); - }, - }); + } + } @@ -90,3 +90,3 @@ describe('ReactDOMComponentTree', function() { return ReactDOMComponentTree.getClosestInstanceFromNode( - renderAndQuery(sel) + renderAndQuery(sel), ); @@ -103,3 +103,5 @@ describe('ReactDOMComponentTree', function() { var root = renderAndQuery(null); - var inst = ReactDOMComponentTree.getInstanceFromNode(root.children[0].childNodes[2]); + var inst = ReactDOMComponentTree.getInstanceFromNode( + root.children[0].childNodes[2], + ); expect(inst._stringText).toBe('goodbye.'); @@ -109,3 +111,2 @@ describe('ReactDOMComponentTree', function() { }); - }); diff --git a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js index bdeb485e2..1187c9bf5 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js @@ -13,19 +13,23 @@ -describe('ReactDOMIDOperations', function() { +describe('ReactDOMIDOperations', () => { + var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); - it('should update innerHTML and preserve whitespace', function() { + it('should update innerHTML and preserve whitespace', () => { var stubNode = document.createElement('div'); - var html = '\n \t \n testContent \t \n \t'; + var stubInstance = {_debugID: 1}; + ReactDOMComponentTree.precacheNode(stubInstance, stubNode); + var html = '\n \t \n testContent \t \n \t'; ReactDOMIDOperations.dangerouslyProcessChildrenUpdates( - {_nativeNode: stubNode}, - [{ - type: ReactMultiChildUpdateTypes.SET_MARKUP, - content: html, - fromIndex: null, - toIndex: null, - }], - [] + stubInstance, + [ + { + type: 'SET_MARKUP', + content: html, + fromIndex: null, + toIndex: null, + }, + ], + [], ); diff --git a/src/renderers/dom/client/__tests__/ReactDOMSVG-test.js b/src/renderers/dom/client/__tests__/ReactDOMSVG-test.js index 84fb2711f..d7904b003 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMSVG-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMSVG-test.js @@ -16,5 +16,4 @@ var ReactDOMServer; -describe('ReactDOMSVG', function() { - - beforeEach(function() { +describe('ReactDOMSVG', () => { + beforeEach(() => { React = require('React'); @@ -23,3 +22,3 @@ describe('ReactDOMSVG', function() { - it('creates initial namespaced markup', function() { + it('creates initial namespaced markup', () => { var markup = ReactDOMServer.renderToString( @@ -27,3 +26,3 @@ describe('ReactDOMSVG', function() { - + , ); @@ -31,3 +30,2 @@ describe('ReactDOMSVG', function() { }); - }); diff --git a/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js b/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js index 888e17d15..ed40c8112 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js @@ -23,4 +23,4 @@ var ARG2 = {arg2: true}; -var ChildComponent = React.createClass({ - render: function() { +class ChildComponent extends React.Component { + render() { return ( @@ -31,7 +31,7 @@ var ChildComponent = React.createClass({ ); - }, -}); + } +} -var ParentComponent = React.createClass({ - render: function() { +class ParentComponent extends React.Component { + render() { return ( @@ -45,4 +45,4 @@ var ParentComponent = React.createClass({ ); - }, -}); + } +} @@ -52,3 +52,3 @@ function renderParentIntoDocument() { -describe('ReactDOMTreeTraversal', function() { +describe('ReactDOMTreeTraversal', () => { var ReactDOMTreeTraversal; @@ -56,6 +56,6 @@ describe('ReactDOMTreeTraversal', function() { var aggregatedArgs; - function argAggregator(inst, isUp, arg) { + function argAggregator(inst, phase, arg) { aggregatedArgs.push({ node: ReactDOMComponentTree.getNodeFromInstance(inst), - isUp: isUp, + phase: phase, arg: arg, @@ -68,3 +68,3 @@ describe('ReactDOMTreeTraversal', function() { - beforeEach(function() { + beforeEach(() => { ReactDOMTreeTraversal = require('ReactDOMTreeTraversal'); @@ -73,4 +73,4 @@ describe('ReactDOMTreeTraversal', function() { - describe('traverseTwoPhase', function() { - it('should not traverse when traversing outside DOM', function() { + describe('traverseTwoPhase', () => { + it('should not traverse when traversing outside DOM', () => { var expectedAggregation = []; @@ -80,3 +80,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should traverse two phase across component boundary', function() { + it('should traverse two phase across component boundary', () => { var parent = renderParentIntoDocument(); @@ -84,11 +84,11 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG}, - {node: parent.refs.P_P1, isUp: false, arg: ARG}, - {node: parent.refs.P_P1_C1.refs.DIV, isUp: false, arg: ARG}, - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: false, arg: ARG}, + {node: parent.refs.P, phase: 'captured', arg: ARG}, + {node: parent.refs.P_P1, phase: 'captured', arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV, phase: 'captured', arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV_1, phase: 'captured', arg: ARG}, - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: true, arg: ARG}, - {node: parent.refs.P_P1_C1.refs.DIV, isUp: true, arg: ARG}, - {node: parent.refs.P_P1, isUp: true, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV_1, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P_P1, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P, phase: 'bubbled', arg: ARG}, ]; @@ -98,3 +98,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should traverse two phase at shallowest node', function() { + it('should traverse two phase at shallowest node', () => { var parent = renderParentIntoDocument(); @@ -102,4 +102,4 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, + {node: parent.refs.P, phase: 'captured', arg: ARG}, + {node: parent.refs.P, phase: 'bubbled', arg: ARG}, ]; @@ -110,4 +110,4 @@ describe('ReactDOMTreeTraversal', function() { - describe('traverseEnterLeave', function() { - it('should not traverse when enter/leaving outside DOM', function() { + describe('traverseEnterLeave', () => { + it('should not traverse when enter/leaving outside DOM', () => { var target = null; @@ -115,3 +115,7 @@ describe('ReactDOMTreeTraversal', function() { ReactDOMTreeTraversal.traverseEnterLeave( - target, target, argAggregator, ARG, ARG2 + target, + target, + argAggregator, + ARG, + ARG2, ); @@ -120,3 +124,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should not traverse if enter/leave the same node', function() { + it('should not traverse if enter/leave the same node', () => { var parent = renderParentIntoDocument(); @@ -126,3 +130,7 @@ describe('ReactDOMTreeTraversal', function() { ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -131,3 +139,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should traverse enter/leave to sibling - avoids parent', function() { + it('should traverse enter/leave to sibling - avoids parent', () => { var parent = renderParentIntoDocument(); @@ -136,8 +144,12 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: true, arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV_1, phase: 'bubbled', arg: ARG}, // enter/leave shouldn't fire anything on the parent - {node: parent.refs.P_P1_C1.refs.DIV_2, isUp: false, arg: ARG2}, + {node: parent.refs.P_P1_C1.refs.DIV_2, phase: 'captured', arg: ARG2}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -146,3 +158,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should traverse enter/leave to parent - avoids parent', function() { + it('should traverse enter/leave to parent - avoids parent', () => { var parent = renderParentIntoDocument(); @@ -151,6 +163,10 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: true, arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV_1, phase: 'bubbled', arg: ARG}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -159,3 +175,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should enter from the window', function() { + it('should enter from the window', () => { var parent = renderParentIntoDocument(); @@ -164,8 +180,12 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG2}, - {node: parent.refs.P_P1, isUp: false, arg: ARG2}, - {node: parent.refs.P_P1_C1.refs.DIV, isUp: false, arg: ARG2}, + {node: parent.refs.P, phase: 'captured', arg: ARG2}, + {node: parent.refs.P_P1, phase: 'captured', arg: ARG2}, + {node: parent.refs.P_P1_C1.refs.DIV, phase: 'captured', arg: ARG2}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -174,3 +194,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should enter from the window to the shallowest', function() { + it('should enter from the window to the shallowest', () => { var parent = renderParentIntoDocument(); @@ -179,6 +199,10 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG2}, + {node: parent.refs.P, phase: 'captured', arg: ARG2}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -187,3 +211,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should leave to the window', function() { + it('should leave to the window', () => { var parent = renderParentIntoDocument(); @@ -192,8 +216,12 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV, isUp: true, arg: ARG}, - {node: parent.refs.P_P1, isUp: true, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P_P1, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P, phase: 'bubbled', arg: ARG}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -202,3 +230,3 @@ describe('ReactDOMTreeTraversal', function() { - it('should leave to the window from the shallowest', function() { + it('should leave to the window from the shallowest', () => { var parent = renderParentIntoDocument(); @@ -207,8 +235,12 @@ describe('ReactDOMTreeTraversal', function() { var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV, isUp: true, arg: ARG}, - {node: parent.refs.P_P1, isUp: true, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, + {node: parent.refs.P_P1_C1.refs.DIV, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P_P1, phase: 'bubbled', arg: ARG}, + {node: parent.refs.P, phase: 'bubbled', arg: ARG}, ]; ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 + leave, + enter, + argAggregator, + ARG, + ARG2, ); @@ -218,4 +250,4 @@ describe('ReactDOMTreeTraversal', function() { - describe('getFirstCommonAncestor', function() { - it('should determine the first common ancestor correctly', function() { + describe('getFirstCommonAncestor', () => { + it('should determine the first common ancestor correctly', () => { var parent = renderParentIntoDocument(); @@ -223,3 +255,4 @@ describe('ReactDOMTreeTraversal', function() { // Common ancestor with self is self. - {one: parent.refs.P_P1_C1.refs.DIV_1, + { + one: parent.refs.P_P1_C1.refs.DIV_1, two: parent.refs.P_P1_C1.refs.DIV_1, @@ -265,3 +298,3 @@ describe('ReactDOMTreeTraversal', function() { getInst(plan.one), - getInst(plan.two) + getInst(plan.two), ); @@ -271,3 +304,2 @@ describe('ReactDOMTreeTraversal', function() { }); - }); diff --git a/src/renderers/dom/client/__tests__/ReactEventIndependence-test.js b/src/renderers/dom/client/__tests__/ReactEventIndependence-test.js index e652e9e1c..ed5ce813f 100644 --- a/src/renderers/dom/client/__tests__/ReactEventIndependence-test.js +++ b/src/renderers/dom/client/__tests__/ReactEventIndependence-test.js @@ -17,4 +17,4 @@ var ReactTestUtils; -describe('ReactEventIndependence', function() { - beforeEach(function() { +describe('ReactEventIndependence', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -26,3 +26,3 @@ describe('ReactEventIndependence', function() { - it('does not crash with other react inside', function() { + it('does not crash with other react inside', () => { var clicks = 0; @@ -34,3 +34,3 @@ describe('ReactEventIndependence', function() { }} - /> + />, ); @@ -40,3 +40,3 @@ describe('ReactEventIndependence', function() { - it('does not crash with other react outside', function() { + it('does not crash with other react outside', () => { var clicks = 0; @@ -46,3 +46,3 @@ describe('ReactEventIndependence', function() { , - outer + outer, ); @@ -52,3 +52,3 @@ describe('ReactEventIndependence', function() { - it('does not when event fired on unmounted tree', function() { + it('does not when event fired on unmounted tree', () => { var clicks = 0; @@ -57,3 +57,3 @@ describe('ReactEventIndependence', function() { , - container + container, ); @@ -68,3 +68,2 @@ describe('ReactEventIndependence', function() { }); - }); diff --git a/src/renderers/dom/client/__tests__/ReactEventListener-test.js b/src/renderers/dom/client/__tests__/ReactEventListener-test.js index 49d4f195d..598da2c6f 100644 --- a/src/renderers/dom/client/__tests__/ReactEventListener-test.js +++ b/src/renderers/dom/client/__tests__/ReactEventListener-test.js @@ -13,6 +13,5 @@ - var EVENT_TARGET_PARAM = 1; -describe('ReactEventListener', function() { +describe('ReactEventListener', () => { var React; @@ -24,3 +23,3 @@ describe('ReactEventListener', function() { - beforeEach(function() { + beforeEach(() => { jest.resetModuleRegistry(); @@ -32,3 +31,3 @@ describe('ReactEventListener', function() { - handleTopLevel = jest.genMockFn(); + handleTopLevel = jest.fn(); ReactEventListener._handleTopLevel = handleTopLevel; @@ -36,3 +35,3 @@ describe('ReactEventListener', function() { - it('should dispatch events from outside React tree', function() { + it('should dispatch events from outside React tree', () => { var otherNode = document.createElement('h1'); @@ -40,15 +39,12 @@ describe('ReactEventListener', function() { expect(handleTopLevel.mock.calls.length).toBe(0); - ReactEventListener.dispatchEvent( - 'topMouseOut', - { - type: 'mouseout', - fromElement: otherNode, - target: otherNode, - srcElement: otherNode, - toElement: ReactDOM.findDOMNode(component), - relatedTarget: ReactDOM.findDOMNode(component), - view: window, - path: [otherNode, otherNode], - }, - ); + ReactEventListener.dispatchEvent('topMouseOut', { + type: 'mouseout', + fromElement: otherNode, + target: otherNode, + srcElement: otherNode, + toElement: ReactDOM.findDOMNode(component), + relatedTarget: ReactDOM.findDOMNode(component), + view: window, + path: [otherNode, otherNode], + }); expect(handleTopLevel.mock.calls.length).toBe(1); @@ -56,4 +52,4 @@ describe('ReactEventListener', function() { - describe('Propagation', function() { - it('should propagate events one level down', function() { + describe('Propagation', () => { + it('should propagate events one level down', () => { var childContainer = document.createElement('div'); @@ -63,4 +59,3 @@ describe('ReactEventListener', function() { childControl = ReactDOM.render(childControl, childContainer); - parentControl = - ReactDOM.render(parentControl, parentContainer); + parentControl = ReactDOM.render(parentControl, parentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); @@ -74,9 +69,11 @@ describe('ReactEventListener', function() { expect(calls.length).toBe(2); - expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(childControl)); - expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); + expect(calls[0][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(childControl), + ); + expect(calls[1][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(parentControl), + ); }); - it('should propagate events two levels down', function() { + it('should propagate events two levels down', () => { var childContainer = document.createElement('div'); @@ -88,6 +85,7 @@ describe('ReactEventListener', function() { childControl = ReactDOM.render(childControl, childContainer); - parentControl = - ReactDOM.render(parentControl, parentContainer); - grandParentControl = - ReactDOM.render(grandParentControl, grandParentContainer); + parentControl = ReactDOM.render(parentControl, parentContainer); + grandParentControl = ReactDOM.render( + grandParentControl, + grandParentContainer, + ); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); @@ -102,11 +100,14 @@ describe('ReactEventListener', function() { expect(calls.length).toBe(3); - expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(childControl)); - expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); - expect(calls[2][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(grandParentControl)); + expect(calls[0][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(childControl), + ); + expect(calls[1][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(parentControl), + ); + expect(calls[2][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(grandParentControl), + ); }); - it('should not get confused by disappearing elements', function() { + it('should not get confused by disappearing elements', () => { var childContainer = document.createElement('div'); @@ -116,4 +117,3 @@ describe('ReactEventListener', function() { childControl = ReactDOM.render(childControl, childContainer); - parentControl = - ReactDOM.render(parentControl, parentContainer); + parentControl = ReactDOM.render(parentControl, parentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); @@ -125,9 +125,12 @@ describe('ReactEventListener', function() { var childNode = ReactDOM.findDOMNode(childControl); - handleTopLevel.mockImplementation( - function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { - if (topLevelTarget === childNode) { - ReactDOM.unmountComponentAtNode(childContainer); - } + handleTopLevel.mockImplementation(function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent, + ) { + if (topLevelTarget === childNode) { + ReactDOM.unmountComponentAtNode(childContainer); } - ); + }); @@ -140,19 +143,15 @@ describe('ReactEventListener', function() { expect(calls.length).toBe(2); - expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(childNode)); - expect(calls[1][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(parentControl)); + expect(calls[0][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(childNode), + ); + expect(calls[1][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(parentControl), + ); }); - it('should batch between handlers from different roots', function() { + it('should batch between handlers from different roots', () => { var childContainer = document.createElement('div'); var parentContainer = document.createElement('div'); - var childControl = ReactDOM.render( -
Child
, - childContainer - ); - var parentControl = ReactDOM.render( -
Parent
, - parentContainer - ); + var childControl = ReactDOM.render(
Child
, childContainer); + var parentControl = ReactDOM.render(
Parent
, parentContainer); ReactDOM.findDOMNode(parentControl).appendChild(childContainer); @@ -162,15 +161,20 @@ describe('ReactEventListener', function() { var childNode = ReactDOM.findDOMNode(childControl); - handleTopLevel.mockImplementation( - function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { - ReactDOM.render( -
{topLevelTarget === childNode ? '1' : '2'}
, - childContainer - ); - // Since we're batching, neither update should yet have gone through. - expect(childNode.textContent).toBe('Child'); - } - ); + handleTopLevel.mockImplementation(function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent, + ) { + ReactDOM.render( +
{topLevelTarget === childNode ? '1' : '2'}
, + childContainer, + ); + // Since we're batching, neither update should yet have gone through. + expect(childNode.textContent).toBe('Child'); + }); - var callback = - ReactEventListener.dispatchEvent.bind(ReactEventListener, 'test'); + var callback = ReactEventListener.dispatchEvent.bind( + ReactEventListener, + 'test', + ); callback({ @@ -185,15 +189,13 @@ describe('ReactEventListener', function() { - it('should not fire duplicate events for a React DOM tree', function() { - var Wrapper = React.createClass({ - - getInner: function() { + it('should not fire duplicate events for a React DOM tree', () => { + class Wrapper extends React.Component { + getInner = () => { return this.refs.inner; - }, + }; - render: function() { + render() { var inner =
Inner
; return
{inner}
; - }, - - }); + } + } @@ -208,4 +210,5 @@ describe('ReactEventListener', function() { expect(calls.length).toBe(1); - expect(calls[0][EVENT_TARGET_PARAM]) - .toBe(ReactDOMComponentTree.getInstanceFromNode(instance.getInner())); + expect(calls[0][EVENT_TARGET_PARAM]).toBe( + ReactDOMComponentTree.getInstanceFromNode(instance.getInner()), + ); }); diff --git a/src/renderers/dom/client/__tests__/ReactMount-test.js b/src/renderers/dom/client/__tests__/ReactMount-test.js index 06b1c1641..d83bd1108 100644 --- a/src/renderers/dom/client/__tests__/ReactMount-test.js +++ b/src/renderers/dom/client/__tests__/ReactMount-test.js @@ -20,4 +20,4 @@ var WebComponents; -describe('ReactMount', function() { - beforeEach(function() { +describe('ReactMount', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -41,4 +41,4 @@ describe('ReactMount', function() { - describe('unmountComponentAtNode', function() { - it('throws when given a non-node', function() { + describe('unmountComponentAtNode', () => { + it('throws when given a non-node', () => { var nodeArray = document.getElementsByTagName('div'); @@ -46,4 +46,4 @@ describe('ReactMount', function() { ReactDOM.unmountComponentAtNode(nodeArray); - }).toThrow( - 'unmountComponentAtNode(...): Target container is not a DOM element.' + }).toThrowError( + 'unmountComponentAtNode(...): Target container is not a DOM element.', ); @@ -52,8 +52,8 @@ describe('ReactMount', function() { - it('throws when given a string', function() { + it('throws when given a string', () => { expect(function() { ReactTestUtils.renderIntoDocument('div'); - }).toThrow( + }).toThrowError( 'ReactDOM.render(): Invalid component element. Instead of passing a ' + - 'string like \'div\', pass React.createElement(\'div\') or
.' + "string like 'div', pass React.createElement('div') or
.", ); @@ -61,13 +61,14 @@ describe('ReactMount', function() { - it('throws when given a factory', function() { - var Component = React.createClass({ - render: function() { + it('throws when given a factory', () => { + class Component extends React.Component { + render() { return
; - }, - }); + } + } + expect(function() { ReactTestUtils.renderIntoDocument(Component); - }).toThrow( + }).toThrowError( 'ReactDOM.render(): Invalid component element. Instead of passing a ' + - 'class like Foo, pass React.createElement(Foo) or .' + 'class like Foo, pass React.createElement(Foo) or .', ); @@ -75,3 +76,3 @@ describe('ReactMount', function() { - it('should render different components in same root', function() { + it('should render different components in same root', () => { var container = document.createElement('container'); @@ -79,6 +80,6 @@ describe('ReactMount', function() { - ReactMount.render(
, container); + ReactMount.render(
, container); expect(container.firstChild.nodeName).toBe('DIV'); - ReactMount.render(, container); + ReactMount.render(, container); expect(container.firstChild.nodeName).toBe('SPAN'); @@ -86,15 +87,15 @@ describe('ReactMount', function() { - it('should unmount and remount if the key changes', function() { + it('should unmount and remount if the key changes', () => { var container = document.createElement('container'); - var mockMount = jest.genMockFn(); - var mockUnmount = jest.genMockFn(); + var mockMount = jest.fn(); + var mockUnmount = jest.fn(); - var Component = React.createClass({ - componentDidMount: mockMount, - componentWillUnmount: mockUnmount, - render: function() { + class Component extends React.Component { + componentDidMount = mockMount; + componentWillUnmount = mockUnmount; + render() { return {this.props.text}; - }, - }); + } + } @@ -121,3 +122,3 @@ describe('ReactMount', function() { - it('should reuse markup if rendering to the same target twice', function() { + it('should reuse markup if rendering to the same target twice', () => { var container = document.createElement('container'); @@ -129,3 +130,3 @@ describe('ReactMount', function() { - it('should warn if mounting into dirty rendered markup', function() { + it('should warn if mounting into dirty rendered markup', () => { var container = document.createElement('container'); @@ -135,3 +136,3 @@ describe('ReactMount', function() { ReactMount.render(
, container); - expect(console.error.calls.length).toBe(1); + expect(console.error.calls.count()).toBe(1); @@ -140,6 +141,6 @@ describe('ReactMount', function() { ReactMount.render(
, container); - expect(console.error.calls.length).toBe(2); + expect(console.error.calls.count()).toBe(2); }); - it('should not warn if mounting into non-empty node', function() { + it('should not warn if mounting into non-empty node', () => { var container = document.createElement('container'); @@ -149,6 +150,6 @@ describe('ReactMount', function() { ReactMount.render(
, container); - expect(console.error.calls.length).toBe(0); + expect(console.error.calls.count()).toBe(0); }); - it('should warn when mounting into document.body', function() { + it('should warn when mounting into document.body', () => { var iFrame = document.createElement('iframe'); @@ -159,5 +160,5 @@ describe('ReactMount', function() { - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'Rendering components directly into document.body is discouraged' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( + 'Rendering components directly into document.body is discouraged', ); @@ -165,6 +166,7 @@ describe('ReactMount', function() { - it('should account for escaping on a checksum mismatch', function() { + it('should account for escaping on a checksum mismatch', () => { var div = document.createElement('div'); var markup = ReactDOMServer.renderToString( -
This markup contains an nbsp entity:   server text
); +
This markup contains an nbsp entity:   server text
, + ); div.innerHTML = markup; @@ -174,8 +176,8 @@ describe('ReactMount', function() {
This markup contains an nbsp entity:   client text
, - div + div, ); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toContain( ' (client) nbsp entity:   client text
\n' + - ' (server) nbsp entity:   server text
' + ' (server) nbsp entity:   server text
', ); @@ -184,3 +186,3 @@ describe('ReactMount', function() { if (WebComponents !== undefined) { - it('should allow mounting/unmounting to document fragment container', function() { + it('should allow mounting/unmounting to document fragment container', () => { var shadowRoot; @@ -206,9 +208,11 @@ describe('ReactMount', function() { - it('should warn if render removes React-rendered children', function() { + it('should warn if render removes React-rendered children', () => { var container = document.createElement('container'); - var Component = React.createClass({ - render: function() { + + class Component extends React.Component { + render() { return
; - }, - }); + } + } + ReactDOM.render(, container); @@ -219,8 +223,8 @@ describe('ReactMount', function() { ReactDOM.render(, rootNode); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( 'Warning: render(...): Replacing React-rendered children with a new ' + - 'root component. If you intended to update the children of this node, ' + - 'you should instead have the existing children update their state and ' + - 'render the new components instead of calling ReactDOM.render.' + 'root component. If you intended to update the children of this node, ' + + 'you should instead have the existing children update their state and ' + + 'render the new components instead of calling ReactDOM.render.', ); @@ -228,3 +232,31 @@ describe('ReactMount', function() { - it('passes the correct callback context', function() { + it('should warn if the unmounted node was rendered by another copy of React', () => { + jest.resetModuleRegistry(); + var ReactDOMOther = require('ReactDOM'); + var container = document.createElement('div'); + + class Component extends React.Component { + render() { + return
; + } + } + + ReactDOM.render(, container); + // Make sure ReactDOM and ReactDOMOther are different copies + expect(ReactDOM).not.toEqual(ReactDOMOther); + + spyOn(console, 'error'); + ReactDOMOther.unmountComponentAtNode(container); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + "Warning: unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); + + // Don't throw a warning if the correct React copy unmounts the node + ReactDOM.unmountComponentAtNode(container); + expect(console.error.calls.count()).toBe(1); + }); + + it('passes the correct callback context', () => { var container = document.createElement('div'); @@ -268,3 +300,3 @@ describe('ReactMount', function() { - it('tracks root instances', function() { + it('tracks root instances', () => { // Used by devtools. @@ -280,16 +312,16 @@ describe('ReactMount', function() { - it('marks top-level mounts', function() { + it('marks top-level mounts', () => { var ReactFeatureFlags = require('ReactFeatureFlags'); - var Foo = React.createClass({ - render: function() { + class Foo extends React.Component { + render() { return ; - }, - }); + } + } - var Bar = React.createClass({ - render: function() { + class Bar extends React.Component { + render() { return
; - }, - }); + } + } @@ -302,6 +334,6 @@ describe('ReactMount', function() { - expect(console.time.argsForCall.length).toBe(1); - expect(console.time.argsForCall[0][0]).toBe('React mount: Foo'); - expect(console.timeEnd.argsForCall.length).toBe(1); - expect(console.timeEnd.argsForCall[0][0]).toBe('React mount: Foo'); + expect(console.time.calls.count()).toBe(1); + expect(console.time.calls.argsFor(0)[0]).toBe('React mount: Foo'); + expect(console.timeEnd.calls.count()).toBe(1); + expect(console.timeEnd.calls.argsFor(0)[0]).toBe('React mount: Foo'); } finally { diff --git a/src/renderers/dom/client/__tests__/ReactMountDestruction-test.js b/src/renderers/dom/client/__tests__/ReactMountDestruction-test.js index a4d8e3997..37cc4f5b9 100644 --- a/src/renderers/dom/client/__tests__/ReactMountDestruction-test.js +++ b/src/renderers/dom/client/__tests__/ReactMountDestruction-test.js @@ -16,4 +16,4 @@ var ReactDOM = require('ReactDOM'); -describe('ReactMount', function() { - it('should destroy a react root upon request', function() { +describe('ReactMount', () => { + it('should destroy a react root upon request', () => { var mainContainerDiv = document.createElement('div'); @@ -42,9 +42,10 @@ describe('ReactMount', function() { - it('should warn when unmounting a non-container root node', function() { + it('should warn when unmounting a non-container root node', () => { var mainContainerDiv = document.createElement('div'); - var component = + var component = (
-
; +
+ ); ReactDOM.render(component, mainContainerDiv); @@ -55,8 +56,8 @@ describe('ReactMount', function() { ReactDOM.unmountComponentAtNode(rootDiv); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: unmountComponentAtNode(): The node you\'re attempting to ' + - 'unmount was rendered by React and is not a top-level container. You ' + - 'may have accidentally passed in a React root node instead of its ' + - 'container.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + "Warning: unmountComponentAtNode(): The node you're attempting to " + + 'unmount was rendered by React and is not a top-level container. You ' + + 'may have accidentally passed in a React root node instead of its ' + + 'container.', ); @@ -64,6 +65,6 @@ describe('ReactMount', function() { - it('should warn when unmounting a non-container, non-root node', function() { + it('should warn when unmounting a non-container, non-root node', () => { var mainContainerDiv = document.createElement('div'); - var component = + var component = (
@@ -72,3 +73,4 @@ describe('ReactMount', function() {
-
; +
+ ); ReactDOM.render(component, mainContainerDiv); @@ -79,8 +81,8 @@ describe('ReactMount', function() { ReactDOM.unmountComponentAtNode(nonRootDiv); - expect(console.error.calls.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: unmountComponentAtNode(): The node you\'re attempting to ' + - 'unmount was rendered by React and is not a top-level container. ' + - 'Instead, have the parent component update its state and rerender in ' + - 'order to remove this component.' + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toBe( + "Warning: unmountComponentAtNode(): The node you're attempting to " + + 'unmount was rendered by React and is not a top-level container. ' + + 'Instead, have the parent component update its state and rerender in ' + + 'order to remove this component.', ); diff --git a/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js b/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js index 1d9682662..8150a17f7 100644 --- a/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js +++ b/src/renderers/dom/client/__tests__/ReactRenderDocument-test.js @@ -28,4 +28,4 @@ var UNMOUNT_INVARIANT_MESSAGE = -describe('rendering React components at document', function() { - beforeEach(function() { +describe('rendering React components at document', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -40,7 +40,7 @@ describe('rendering React components at document', function() { - it('should be able to adopt server markup', function() { + it('should be able to adopt server markup', () => { expect(testDocument).not.toBeUndefined(); - var Root = React.createClass({ - render: function() { + class Root extends React.Component { + render() { return ( @@ -55,4 +55,4 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } @@ -71,7 +71,7 @@ describe('rendering React components at document', function() { - it('should not be able to unmount component from document node', function() { + it('should not be able to unmount component from document node', () => { expect(testDocument).not.toBeUndefined(); - var Root = React.createClass({ - render: function() { + class Root extends React.Component { + render() { return ( @@ -86,4 +86,4 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } @@ -96,3 +96,3 @@ describe('rendering React components at document', function() { ReactDOM.unmountComponentAtNode(testDocument); - }).toThrow(UNMOUNT_INVARIANT_MESSAGE); + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); @@ -101,7 +101,7 @@ describe('rendering React components at document', function() { - it('should not be able to switch root constructors', function() { + it('should not be able to switch root constructors', () => { expect(testDocument).not.toBeUndefined(); - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { return ( @@ -116,7 +116,7 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } - var Component2 = React.createClass({ - render: function() { + class Component2 extends React.Component { + render() { return ( @@ -131,4 +131,4 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } @@ -144,3 +144,3 @@ describe('rendering React components at document', function() { ReactDOM.render(, testDocument); - }).toThrow(UNMOUNT_INVARIANT_MESSAGE); + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); @@ -149,7 +149,7 @@ describe('rendering React components at document', function() { - it('should be able to mount into document', function() { + it('should be able to mount into document', () => { expect(testDocument).not.toBeUndefined(); - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { return ( @@ -164,7 +164,7 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } var markup = ReactDOMServer.renderToString( - + , ); @@ -177,7 +177,7 @@ describe('rendering React components at document', function() { - it('should give helpful errors on state desync', function() { + it('should give helpful errors on state desync', () => { expect(testDocument).not.toBeUndefined(); - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { return ( @@ -192,7 +192,7 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } var markup = ReactDOMServer.renderToString( - + , ); @@ -203,13 +203,13 @@ describe('rendering React components at document', function() { ReactDOM.render(, testDocument); - }).toThrow( - 'You\'re trying to render a component to the document using ' + - 'server rendering but the checksum was invalid. This usually ' + - 'means you rendered a different component type or props on ' + - 'the client from the one on the server, or your render() methods ' + - 'are impure. React cannot handle this case due to cross-browser ' + - 'quirks by rendering at the document root. You should look for ' + - 'environment dependent code in your components and ensure ' + - 'the props are the same client and server side:\n' + - ' (client) dy data-reactid="4">Hello worldGoodbye world' + }).toThrowError( + "You're trying to render a component to the document using " + + 'server rendering but the checksum was invalid. This usually ' + + 'means you rendered a different component type or props on ' + + 'the client from the one on the server, or your render() methods ' + + 'are impure. React cannot handle this case due to cross-browser ' + + 'quirks by rendering at the document root. You should look for ' + + 'environment dependent code in your components and ensure ' + + 'the props are the same client and server side:\n' + + ' (client) dy data-reactid="4">Hello worldGoodbye world', ); @@ -217,3 +217,3 @@ describe('rendering React components at document', function() { - it('should throw on full document render w/ no markup', function() { + it('should throw on full document render w/ no markup', () => { expect(testDocument).not.toBeUndefined(); @@ -222,4 +222,4 @@ describe('rendering React components at document', function() { - var Component = React.createClass({ - render: function() { + class Component extends React.Component { + render() { return ( @@ -234,4 +234,4 @@ describe('rendering React components at document', function() { ); - }, - }); + } + } @@ -239,7 +239,7 @@ describe('rendering React components at document', function() { ReactDOM.render(, container); - }).toThrow( - 'You\'re trying to render a component to the document but you didn\'t ' + - 'use server rendering. We can\'t do this without using server ' + - 'rendering due to cross-browser quirks. See ' + - 'ReactDOMServer.renderToString() for server rendering.' + }).toThrowError( + "You're trying to render a component to the document but you didn't " + + "use server rendering. We can't do this without using server " + + 'rendering due to cross-browser quirks. See ' + + 'ReactDOMServer.renderToString() for server rendering.', ); @@ -247,4 +247,4 @@ describe('rendering React components at document', function() { - it('supports findDOMNode on full-page components', function() { - var tree = + it('supports findDOMNode on full-page components', () => { + var tree = ( @@ -256,3 +256,4 @@ describe('rendering React components at document', function() { - ; + + ); diff --git a/src/renderers/dom/client/__tests__/findDOMNode-test.js b/src/renderers/dom/client/__tests__/findDOMNode-test.js index 1f861dd34..502e24fe7 100644 --- a/src/renderers/dom/client/__tests__/findDOMNode-test.js +++ b/src/renderers/dom/client/__tests__/findDOMNode-test.js @@ -17,4 +17,4 @@ var ReactTestUtils = require('ReactTestUtils'); -describe('findDOMNode', function() { - it('findDOMNode should return null if passed null', function() { +describe('findDOMNode', () => { + it('findDOMNode should return null if passed null', () => { expect(ReactDOM.findDOMNode(null)).toBe(null); @@ -22,8 +22,8 @@ describe('findDOMNode', function() { - it('findDOMNode should find dom element', function() { - var MyNode = React.createClass({ - render: function() { + it('findDOMNode should find dom element', () => { + class MyNode extends React.Component { + render() { return
Noise
; - }, - }); + } + } @@ -36,7 +36,7 @@ describe('findDOMNode', function() { - it('findDOMNode should reject random objects', function() { + it('findDOMNode should reject random objects', () => { expect(function() { ReactDOM.findDOMNode({foo: 'bar'}); - }).toThrow( - 'Element appears to be neither ReactComponent nor DOMNode (keys: foo)' + }).toThrowError( + 'Element appears to be neither ReactComponent nor DOMNode (keys: foo)', ); @@ -44,8 +44,8 @@ describe('findDOMNode', function() { - it('findDOMNode should reject unmounted objects with render func', function() { - var Foo = React.createClass({ - render: function() { + it('findDOMNode should reject unmounted objects with render func', () => { + class Foo extends React.Component { + render() { return
; - }, - }); + } + } @@ -55,4 +55,4 @@ describe('findDOMNode', function() { - expect(() => ReactDOM.findDOMNode(inst)).toThrow( - 'findDOMNode was called on an unmounted component.' + expect(() => ReactDOM.findDOMNode(inst)).toThrowError( + 'findDOMNode was called on an unmounted component.', ); @@ -60,15 +60,15 @@ describe('findDOMNode', function() { - it('findDOMNode should not throw an error when called within a component that is not mounted', function() { - var Bar = React.createClass({ - componentWillMount: function() { + it('findDOMNode should not throw an error when called within a component that is not mounted', () => { + class Bar extends React.Component { + componentWillMount() { expect(ReactDOM.findDOMNode(this)).toBeNull(); - }, - render: function() { - return
; - }, - }); + } - expect(() => ReactTestUtils.renderIntoDocument()).not.toThrow(); - }); + render() { + return
; + } + } + expect(() => ReactTestUtils.renderIntoDocument()).not.toThrow(); + }); }); diff --git a/src/renderers/dom/client/__tests__/inputValueTracking-test.js b/src/renderers/dom/client/__tests__/inputValueTracking-test.js new file mode 100644 index 000000000..103964cc1 --- /dev/null +++ b/src/renderers/dom/client/__tests__/inputValueTracking-test.js @@ -0,0 +1,158 @@ +/** + * Copyright 2013-present, 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 React = require('React'); +var ReactTestUtils = require('ReactTestUtils'); +var inputValueTracking = require('inputValueTracking'); + +describe('inputValueTracking', function() { + var input, checkbox, mockComponent; + + beforeEach(function() { + input = document.createElement('input'); + input.type = 'text'; + checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + mockComponent = {_hostNode: input, _wrapperState: {}}; + }); + + it('should attach tracker to wrapper state', function() { + inputValueTracking.track(mockComponent); + + expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe( + true, + ); + }); + + it('should define `value` on the instance node', function() { + inputValueTracking.track(mockComponent); + + expect(input.hasOwnProperty('value')).toBe(true); + }); + + it('should define `checked` on the instance node', function() { + mockComponent._hostNode = checkbox; + inputValueTracking.track(mockComponent); + + expect(checkbox.hasOwnProperty('checked')).toBe(true); + }); + + it('should initialize with the current value', function() { + input.value = 'foo'; + + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + expect(tracker.getValue()).toEqual('foo'); + }); + + it('should initialize with the current `checked`', function() { + mockComponent._hostNode = checkbox; + checkbox.checked = true; + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + expect(tracker.getValue()).toEqual('true'); + }); + + it('should track value changes', function() { + input.value = 'foo'; + + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + input.value = 'bar'; + expect(tracker.getValue()).toEqual('bar'); + }); + + it('should tracked`checked` changes', function() { + mockComponent._hostNode = checkbox; + checkbox.checked = true; + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + checkbox.checked = false; + expect(tracker.getValue()).toEqual('false'); + }); + + it('should update value manually', function() { + input.value = 'foo'; + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + tracker.setValue('bar'); + expect(tracker.getValue()).toEqual('bar'); + }); + + it('should coerce value to a string', function() { + input.value = 'foo'; + inputValueTracking.track(mockComponent); + + var tracker = mockComponent._wrapperState.valueTracker; + + tracker.setValue(500); + expect(tracker.getValue()).toEqual('500'); + }); + + it('should update value if it changed and return result', function() { + inputValueTracking.track(mockComponent); + input.value = 'foo'; + + var tracker = mockComponent._wrapperState.valueTracker; + + expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(false); + + tracker.setValue('bar'); + + expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(true); + + expect(tracker.getValue()).toEqual('foo'); + }); + + it('should track value and return true when updating untracked instance', function() { + input.value = 'foo'; + + expect(inputValueTracking.updateValueIfChanged(mockComponent)).toBe(true); + + var tracker = mockComponent._wrapperState.valueTracker; + expect(tracker.getValue()).toEqual('foo'); + }); + + it('should return tracker from node', function() { + var node = ReactTestUtils.renderIntoDocument( + , + ); + var tracker = inputValueTracking._getTrackerFromNode(node); + expect(tracker.getValue()).toEqual('foo'); + }); + + it('should stop tracking', function() { + inputValueTracking.track(mockComponent); + + expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe( + true, + ); + + inputValueTracking.stopTracking(mockComponent); + + expect(mockComponent._wrapperState.hasOwnProperty('valueTracker')).toBe( + false, + ); + + expect(input.hasOwnProperty('value')).toBe(false); + }); +}); diff --git a/src/renderers/dom/client/__tests__/validateDOMNesting-test.js b/src/renderers/dom/client/__tests__/validateDOMNesting-test.js index e0bdfe959..97aac29a5 100644 --- a/src/renderers/dom/client/__tests__/validateDOMNesting-test.js +++ b/src/renderers/dom/client/__tests__/validateDOMNesting-test.js @@ -17,12 +17,85 @@ var validateDOMNesting; var specialTags = [ - 'address', 'applet', 'area', 'article', 'aside', 'base', 'basefont', - 'bgsound', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'col', - 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', - 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', - 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', - 'img', 'input', 'isindex', 'li', 'link', 'listing', 'main', 'marquee', 'menu', - 'menuitem', 'meta', 'nav', 'noembed', 'noframes', 'noscript', 'object', 'ol', - 'p', 'param', 'plaintext', 'pre', 'script', 'section', 'select', 'source', - 'style', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', - 'th', 'thead', 'title', 'tr', 'track', 'ul', 'wbr', 'xmp', + 'address', + 'applet', + 'area', + 'article', + 'aside', + 'base', + 'basefont', + 'bgsound', + 'blockquote', + 'body', + 'br', + 'button', + 'caption', + 'center', + 'col', + 'colgroup', + 'dd', + 'details', + 'dir', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'figcaption', + 'figure', + 'footer', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'iframe', + 'img', + 'input', + 'isindex', + 'li', + 'link', + 'listing', + 'main', + 'marquee', + 'menu', + 'menuitem', + 'meta', + 'nav', + 'noembed', + 'noframes', + 'noscript', + 'object', + 'ol', + 'p', + 'param', + 'plaintext', + 'pre', + 'script', + 'section', + 'select', + 'source', + 'style', + 'summary', + 'table', + 'tbody', + 'td', + 'template', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'title', + 'tr', + 'track', + 'ul', + 'wbr', + 'xmp', ]; @@ -31,4 +104,16 @@ var specialTags = [ var formattingTags = [ - 'a', 'b', 'big', 'code', 'em', 'font', 'i', 'nobr', 's', 'small', 'strike', - 'strong', 'tt', 'u', + 'a', + 'b', + 'big', + 'code', + 'em', + 'font', + 'i', + 'nobr', + 's', + 'small', + 'strike', + 'strong', + 'tt', + 'u', ]; @@ -41,4 +126,7 @@ function isTagStackValid(stack) { } - ancestorInfo = - validateDOMNesting.updatedAncestorInfo(ancestorInfo, stack[i], null); + ancestorInfo = validateDOMNesting.updatedAncestorInfo( + ancestorInfo, + stack[i], + null, + ); } @@ -47,4 +135,4 @@ function isTagStackValid(stack) { -describe('ReactContextValidator', function() { - beforeEach(function() { +describe('ReactContextValidator', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -54,3 +142,3 @@ describe('ReactContextValidator', function() { - it('allows any tag with no context', function() { + it('allows any tag with no context', () => { // With renderToString (for example), we don't know where we're mounting the @@ -63,3 +151,3 @@ describe('ReactContextValidator', function() { - it('allows valid nestings', function() { + it('allows valid nestings', () => { expect(isTagStackValid(['table', 'tbody', 'tr', 'td', 'b'])).toBe(true); @@ -78,3 +166,3 @@ describe('ReactContextValidator', function() { - it('prevents problematic nestings', function() { + it('prevents problematic nestings', () => { expect(isTagStackValid(['a', 'a'])).toBe(false); @@ -85,2 +173,4 @@ describe('ReactContextValidator', function() { expect(isTagStackValid(['div', 'html'])).toBe(false); + expect(isTagStackValid(['body', 'body'])).toBe(false); + expect(isTagStackValid(['svg', 'foreignObject', 'body', 'p'])).toBe(false); }); diff --git a/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js b/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js index 794e13661..5f0df7e47 100644 --- a/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/BeforeInputEventPlugin.js @@ -13,3 +13,2 @@ -var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); @@ -20,3 +19,3 @@ var SyntheticInputEvent = require('SyntheticInputEvent'); -var keyOf = require('keyOf'); +import type {TopLevelTypes} from 'EventConstants'; @@ -25,6 +24,4 @@ var START_KEYCODE = 229; -var canUseCompositionEvent = ( - ExecutionEnvironment.canUseDOM && - 'CompositionEvent' in window -); +var canUseCompositionEvent = + ExecutionEnvironment.canUseDOM && 'CompositionEvent' in window; @@ -38,3 +35,3 @@ if (ExecutionEnvironment.canUseDOM && 'documentMode' in document) { // useful, so we don't use it. -var canUseTextInputEvent = ( +var canUseTextInputEvent = ExecutionEnvironment.canUseDOM && @@ -42,4 +39,3 @@ var canUseTextInputEvent = ( !documentMode && - !isPresto() -); + !isPresto(); @@ -48,9 +44,6 @@ var canUseTextInputEvent = ( // spaces, for instance (\u3000) are not recorded correctly. -var useFallbackCompositionData = ( +var useFallbackCompositionData = ExecutionEnvironment.canUseDOM && - ( - !canUseCompositionEvent || - (documentMode && documentMode > 8 && documentMode <= 11) - ) -); + (!canUseCompositionEvent || + (documentMode && documentMode > 8 && documentMode <= 11)); @@ -72,4 +65,2 @@ var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE); -var topLevelTypes = EventConstants.topLevelTypes; - // Events and their corresponding property names. @@ -78,10 +69,10 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onBeforeInput: null}), - captured: keyOf({onBeforeInputCapture: null}), + bubbled: 'onBeforeInput', + captured: 'onBeforeInputCapture', }, dependencies: [ - topLevelTypes.topCompositionEnd, - topLevelTypes.topKeyPress, - topLevelTypes.topTextInput, - topLevelTypes.topPaste, + 'topCompositionEnd', + 'topKeyPress', + 'topTextInput', + 'topPaste', ], @@ -90,12 +81,12 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onCompositionEnd: null}), - captured: keyOf({onCompositionEndCapture: null}), + bubbled: 'onCompositionEnd', + captured: 'onCompositionEndCapture', }, dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionEnd, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown, + 'topBlur', + 'topCompositionEnd', + 'topKeyDown', + 'topKeyPress', + 'topKeyUp', + 'topMouseDown', ], @@ -104,12 +95,12 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onCompositionStart: null}), - captured: keyOf({onCompositionStartCapture: null}), + bubbled: 'onCompositionStart', + captured: 'onCompositionStartCapture', }, dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionStart, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown, + 'topBlur', + 'topCompositionStart', + 'topKeyDown', + 'topKeyPress', + 'topKeyUp', + 'topMouseDown', ], @@ -118,12 +109,12 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onCompositionUpdate: null}), - captured: keyOf({onCompositionUpdateCapture: null}), + bubbled: 'onCompositionUpdate', + captured: 'onCompositionUpdateCapture', }, dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topCompositionUpdate, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyPress, - topLevelTypes.topKeyUp, - topLevelTypes.topMouseDown, + 'topBlur', + 'topCompositionUpdate', + 'topKeyDown', + 'topKeyPress', + 'topKeyUp', + 'topMouseDown', ], @@ -148,3 +139,2 @@ function isKeypressCommand(nativeEvent) { - /** @@ -157,7 +147,7 @@ function getCompositionEventType(topLevelType) { switch (topLevelType) { - case topLevelTypes.topCompositionStart: + case 'topCompositionStart': return eventTypes.compositionStart; - case topLevelTypes.topCompositionEnd: + case 'topCompositionEnd': return eventTypes.compositionEnd; - case topLevelTypes.topCompositionUpdate: + case 'topCompositionUpdate': return eventTypes.compositionUpdate; @@ -175,6 +165,3 @@ function getCompositionEventType(topLevelType) { function isFallbackCompositionStart(topLevelType, nativeEvent) { - return ( - topLevelType === topLevelTypes.topKeyDown && - nativeEvent.keyCode === START_KEYCODE - ); + return topLevelType === 'topKeyDown' && nativeEvent.keyCode === START_KEYCODE; } @@ -190,12 +177,12 @@ function isFallbackCompositionEnd(topLevelType, nativeEvent) { switch (topLevelType) { - case topLevelTypes.topKeyUp: + case 'topKeyUp': // Command keys insert or clear IME input. - return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); - case topLevelTypes.topKeyDown: + return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1; + case 'topKeyDown': // Expect IME keyCode on each keydown. If we get any other // code we must have exited earlier. - return (nativeEvent.keyCode !== START_KEYCODE); - case topLevelTypes.topKeyPress: - case topLevelTypes.topMouseDown: - case topLevelTypes.topBlur: + return nativeEvent.keyCode !== START_KEYCODE; + case 'topKeyPress': + case 'topMouseDown': + case 'topBlur': // Events are not possible without cancelling IME. @@ -234,3 +221,3 @@ function extractCompositionEvent( nativeEvent, - nativeEventTarget + nativeEventTarget, ) { @@ -257,4 +244,5 @@ function extractCompositionEvent( if (!currentComposition && eventType === eventTypes.compositionStart) { - currentComposition = - FallbackCompositionState.getPooled(nativeEventTarget); + currentComposition = FallbackCompositionState.getPooled( + nativeEventTarget, + ); } else if (eventType === eventTypes.compositionEnd) { @@ -270,3 +258,3 @@ function extractCompositionEvent( nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -293,7 +281,7 @@ function extractCompositionEvent( */ -function getNativeBeforeInputChars(topLevelType, nativeEvent) { +function getNativeBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { switch (topLevelType) { - case topLevelTypes.topCompositionEnd: + case 'topCompositionEnd': return getDataFromCustomEvent(nativeEvent); - case topLevelTypes.topKeyPress: + case 'topKeyPress': /** @@ -320,3 +308,3 @@ function getNativeBeforeInputChars(topLevelType, nativeEvent) { - case topLevelTypes.topTextInput: + case 'topTextInput': // Record the characters to be added to the DOM. @@ -347,9 +335,12 @@ function getNativeBeforeInputChars(topLevelType, nativeEvent) { */ -function getFallbackBeforeInputChars(topLevelType, nativeEvent) { +function getFallbackBeforeInputChars(topLevelType: TopLevelTypes, nativeEvent) { // If we are currently composing (IME) and using a fallback to do so, // try to extract the composed characters from the fallback object. + // If composition event is available, we extract a string only at + // compositionevent, otherwise extract it at fallback events. if (currentComposition) { if ( - topLevelType === topLevelTypes.topCompositionEnd || - isFallbackCompositionEnd(topLevelType, nativeEvent) + topLevelType === 'topCompositionEnd' || + (!canUseCompositionEvent && + isFallbackCompositionEnd(topLevelType, nativeEvent)) ) { @@ -364,3 +355,3 @@ function getFallbackBeforeInputChars(topLevelType, nativeEvent) { switch (topLevelType) { - case topLevelTypes.topPaste: + case 'topPaste': // If a paste event occurs after a keypress, throw out the input @@ -368,3 +359,3 @@ function getFallbackBeforeInputChars(topLevelType, nativeEvent) { return null; - case topLevelTypes.topKeyPress: + case 'topKeyPress': /** @@ -389,3 +380,3 @@ function getFallbackBeforeInputChars(topLevelType, nativeEvent) { return null; - case topLevelTypes.topCompositionEnd: + case 'topCompositionEnd': return useFallbackCompositionData ? null : nativeEvent.data; @@ -406,3 +397,3 @@ function extractBeforeInputEvent( nativeEvent, - nativeEventTarget + nativeEventTarget, ) { @@ -426,3 +417,3 @@ function extractBeforeInputEvent( nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -453,3 +444,2 @@ function extractBeforeInputEvent( var BeforeInputEventPlugin = { - eventTypes: eventTypes, @@ -460,3 +450,3 @@ var BeforeInputEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ) { @@ -467,3 +457,3 @@ var BeforeInputEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ), @@ -473,3 +463,3 @@ var BeforeInputEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ), diff --git a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js index 1a6855d28..473aef515 100644 --- a/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js @@ -13,3 +13,2 @@ -var EventConstants = require('EventConstants'); var EventPluginHub = require('EventPluginHub'); @@ -21,2 +20,3 @@ var SyntheticEvent = require('SyntheticEvent'); +var inputValueTracking = require('inputValueTracking'); var getEventTarget = require('getEventTarget'); @@ -24,5 +24,2 @@ var isEventSupported = require('isEventSupported'); var isTextInputElement = require('isTextInputElement'); -var keyOf = require('keyOf'); - -var topLevelTypes = EventConstants.topLevelTypes; @@ -31,14 +28,14 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onChange: null}), - captured: keyOf({onChangeCapture: null}), + bubbled: 'onChange', + captured: 'onChangeCapture', }, dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topChange, - topLevelTypes.topClick, - topLevelTypes.topFocus, - topLevelTypes.topInput, - topLevelTypes.topKeyDown, - topLevelTypes.topKeyUp, - topLevelTypes.topSelectionChange, + 'topBlur', + 'topChange', + 'topClick', + 'topFocus', + 'topInput', + 'topKeyDown', + 'topKeyUp', + 'topSelectionChange', ], @@ -47,2 +44,13 @@ var eventTypes = { +function createAndAccumulateChangeEvent(inst, nativeEvent, target) { + var event = SyntheticEvent.getPooled( + eventTypes.change, + inst, + nativeEvent, + target, + ); + event.type = 'change'; + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; +} /** @@ -52,4 +60,2 @@ var activeElement = null; var activeElementInst = null; -var activeElementValue = null; -var activeElementValueProp = null; @@ -61,4 +67,3 @@ function shouldUseChangeEvent(elem) { return ( - nodeName === 'select' || - (nodeName === 'input' && elem.type === 'file') + nodeName === 'select' || (nodeName === 'input' && elem.type === 'file') ); @@ -69,5 +74,5 @@ if (ExecutionEnvironment.canUseDOM) { // See `handleChange` comment below - doesChangeEventBubble = isEventSupported('change') && ( - !('documentMode' in document) || document.documentMode > 8 - ); + doesChangeEventBubble = + isEventSupported('change') && + (!document.documentMode || document.documentMode > 8); } @@ -75,9 +80,7 @@ if (ExecutionEnvironment.canUseDOM) { function manualDispatchChangeEvent(nativeEvent) { - var event = SyntheticEvent.getPooled( - eventTypes.change, + var event = createAndAccumulateChangeEvent( activeElementInst, nativeEvent, - getEventTarget(nativeEvent) + getEventTarget(nativeEvent), ); - EventPropagators.accumulateTwoPhaseDispatches(event); @@ -117,7 +120,9 @@ function stopWatchingForChangeEventIE8() { -function getTargetInstForChangeEvent( - topLevelType, - targetInst -) { - if (topLevelType === topLevelTypes.topChange) { +function getInstIfValueChanged(targetInst, nativeEvent) { + var updated = inputValueTracking.updateValueIfChanged(targetInst); + var simulated = + nativeEvent.simulated === true && + ChangeEventPlugin._allowSimulatedPassThrough; + + if (updated || simulated) { return targetInst; @@ -125,8 +130,11 @@ function getTargetInstForChangeEvent( } -function handleEventsForChangeEventIE8( - topLevelType, - target, - targetInst -) { - if (topLevelType === topLevelTypes.topFocus) { + +function getTargetInstForChangeEvent(topLevelType, targetInst) { + if (topLevelType === 'topChange') { + return targetInst; + } +} + +function handleEventsForChangeEventIE8(topLevelType, target, targetInst) { + if (topLevelType === 'topFocus') { // stopWatching() should be a noop here but we call it just in case we @@ -135,3 +143,3 @@ function handleEventsForChangeEventIE8( startWatchingForChangeEventIE8(target, targetInst); - } else if (topLevelType === topLevelTypes.topBlur) { + } else if (topLevelType === 'topBlur') { stopWatchingForChangeEventIE8(); @@ -140,3 +148,2 @@ function handleEventsForChangeEventIE8( - /** @@ -148,26 +155,10 @@ if (ExecutionEnvironment.canUseDOM) { // deleting text, so we ignore its input events. - // IE10+ fire input events to often, such when a placeholder - // changes or when an input with a placeholder is focused. - isInputEventSupported = isEventSupported('input') && ( - !('documentMode' in document) || document.documentMode > 11 - ); -} -/** - * (For IE <=11) Replacement getter/setter for the `value` property that gets - * set on the active element. - */ -var newValueProp = { - get: function() { - return activeElementValueProp.get.call(this); - }, - set: function(val) { - // Cast to a string so we can do equality checks. - activeElementValue = '' + val; - activeElementValueProp.set.call(this, val); - }, -}; + isInputEventSupported = + isEventSupported('input') && + (!('documentMode' in document) || document.documentMode > 9); +} /** - * (For IE <=11) Starts tracking propertychange events on the passed-in element + * (For IE <=9) Starts tracking propertychange events on the passed-in element * and override the value property so that we can distinguish user events from @@ -178,16 +169,3 @@ function startWatchingForValueChange(target, targetInst) { activeElementInst = targetInst; - activeElementValue = target.value; - activeElementValueProp = Object.getOwnPropertyDescriptor( - target.constructor.prototype, - 'value' - ); - - // Not guarded in a canDefineProperty check: IE8 supports defineProperty only - // on DOM elements - Object.defineProperty(activeElement, 'value', newValueProp); - if (activeElement.attachEvent) { - activeElement.attachEvent('onpropertychange', handlePropertyChange); - } else { - activeElement.addEventListener('propertychange', handlePropertyChange, false); - } + activeElement.attachEvent('onpropertychange', handlePropertyChange); } @@ -195,3 +173,3 @@ function startWatchingForValueChange(target, targetInst) { /** - * (For IE <=11) Removes the event listeners from the currently-tracked element, + * (For IE <=9) Removes the event listeners from the currently-tracked element, * if any exists. @@ -202,11 +180,3 @@ function stopWatchingForValueChange() { } - - // delete restores the original property definition - delete activeElement.value; - - if (activeElement.detachEvent) { - activeElement.detachEvent('onpropertychange', handlePropertyChange); - } else { - activeElement.removeEventListener('propertychange', handlePropertyChange, false); - } + activeElement.detachEvent('onpropertychange', handlePropertyChange); @@ -214,4 +184,2 @@ function stopWatchingForValueChange() { activeElementInst = null; - activeElementValue = null; - activeElementValueProp = null; } @@ -219,3 +187,3 @@ function stopWatchingForValueChange() { /** - * (For IE <=11) Handles a propertychange event, sending a `change` event if + * (For IE <=9) Handles a propertychange event, sending a `change` event if * the value of the active element has changed. @@ -226,22 +194,4 @@ function handlePropertyChange(nativeEvent) { } - var value = nativeEvent.srcElement.value; - if (value === activeElementValue) { - return; - } - activeElementValue = value; - - manualDispatchChangeEvent(nativeEvent); -} - -/** - * If a `change` event should be fired, returns the target's ID. - */ -function getTargetInstForInputEvent( - topLevelType, - targetInst -) { - if (topLevelType === topLevelTypes.topInput) { - // In modern browsers (i.e., not IE8 or IE9), the input event is exactly - // what we want so fall through here and trigger an abstract event - return targetInst; + if (getInstIfValueChanged(activeElementInst, nativeEvent)) { + manualDispatchChangeEvent(nativeEvent); } @@ -249,8 +199,4 @@ function getTargetInstForInputEvent( -function handleEventsForInputEventIE( - topLevelType, - target, - targetInst -) { - if (topLevelType === topLevelTypes.topFocus) { +function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) { + if (topLevelType === 'topFocus') { // In IE8, we can capture almost all .value changes by adding a @@ -258,3 +204,3 @@ function handleEventsForInputEventIE( // equal to 'value' - // In IE9-11, propertychange fires for most input events but is buggy and + // In IE9, propertychange fires for most input events but is buggy and // doesn't fire when text is deleted, but conveniently, selectionchange @@ -270,3 +216,3 @@ function handleEventsForInputEventIE( startWatchingForValueChange(target, targetInst); - } else if (topLevelType === topLevelTypes.topBlur) { + } else if (topLevelType === 'topBlur') { stopWatchingForValueChange(); @@ -276,9 +222,12 @@ function handleEventsForInputEventIE( // For IE8 and IE9. -function getTargetInstForInputEventIE( +function getTargetInstForInputEventPolyfill( topLevelType, - targetInst + targetInst, + nativeEvent, ) { - if (topLevelType === topLevelTypes.topSelectionChange || - topLevelType === topLevelTypes.topKeyUp || - topLevelType === topLevelTypes.topKeyDown) { + if ( + topLevelType === 'topSelectionChange' || + topLevelType === 'topKeyUp' || + topLevelType === 'topKeyDown' + ) { // On the selectionchange event, the target is just document which isn't @@ -293,6 +242,3 @@ function getTargetInstForInputEventIE( // fire selectionchange normally. - if (activeElement && activeElement.value !== activeElementValue) { - activeElementValue = activeElement.value; - return activeElementInst; - } + return getInstIfValueChanged(activeElementInst, nativeEvent); } @@ -300,3 +246,2 @@ function getTargetInstForInputEventIE( - /** @@ -308,4 +253,6 @@ function shouldUseClickEvent(elem) { // until `blur` in IE8. + var nodeName = elem.nodeName; return ( - (elem.nodeName && elem.nodeName.toLowerCase() === 'input') && + nodeName && + nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio') @@ -314,8 +261,35 @@ function shouldUseClickEvent(elem) { -function getTargetInstForClickEvent( +function getTargetInstForClickEvent(topLevelType, targetInst, nativeEvent) { + if (topLevelType === 'topClick') { + return getInstIfValueChanged(targetInst, nativeEvent); + } +} + +function getTargetInstForInputOrChangeEvent( topLevelType, - targetInst + targetInst, + nativeEvent, ) { - if (topLevelType === topLevelTypes.topClick) { - return targetInst; + if (topLevelType === 'topInput' || topLevelType === 'topChange') { + return getInstIfValueChanged(targetInst, nativeEvent); + } +} + +function handleControlledInputBlur(inst, node) { + // TODO: In IE, inst is occasionally null. Why? + if (inst == null) { + return; + } + + // Fiber and ReactDOM keep wrapper state in separate places + let state = inst._wrapperState || node._wrapperState; + + if (!state || !state.controlled || node.type !== 'number') { + return; + } + + // If controlled, assign the value attribute to the current value on blur + let value = '' + node.value; + if (node.getAttribute('value') !== value) { + node.setAttribute('value', value); } @@ -334,5 +308,7 @@ function getTargetInstForClickEvent( var ChangeEventPlugin = { - eventTypes: eventTypes, + _allowSimulatedPassThrough: true, + _isInputEventSupported: isInputEventSupported, + extractEvents: function( @@ -341,6 +317,7 @@ var ChangeEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ) { - var targetNode = targetInst ? - ReactDOMComponentTree.getNodeFromInstance(targetInst) : window; + var targetNode = targetInst + ? ReactDOMComponentTree.getNodeFromInstance(targetInst) + : window; @@ -355,6 +332,6 @@ var ChangeEventPlugin = { if (isInputEventSupported) { - getTargetInstFunc = getTargetInstForInputEvent; + getTargetInstFunc = getTargetInstForInputOrChangeEvent; } else { - getTargetInstFunc = getTargetInstForInputEventIE; - handleEventFunc = handleEventsForInputEventIE; + getTargetInstFunc = getTargetInstForInputEventPolyfill; + handleEventFunc = handleEventsForInputEventPolyfill; } @@ -365,12 +342,9 @@ var ChangeEventPlugin = { if (getTargetInstFunc) { - var inst = getTargetInstFunc(topLevelType, targetInst); + var inst = getTargetInstFunc(topLevelType, targetInst, nativeEvent); if (inst) { - var event = SyntheticEvent.getPooled( - eventTypes.change, + var event = createAndAccumulateChangeEvent( inst, nativeEvent, - nativeEventTarget + nativeEventTarget, ); - event.type = 'change'; - EventPropagators.accumulateTwoPhaseDispatches(event); return event; @@ -380,10 +354,10 @@ var ChangeEventPlugin = { if (handleEventFunc) { - handleEventFunc( - topLevelType, - targetNode, - targetInst - ); + handleEventFunc(topLevelType, targetNode, targetInst); } - }, + // When blurring, set the value attribute for number inputs + if (topLevelType === 'topBlur') { + handleControlledInputBlur(targetInst, targetNode); + } + }, }; diff --git a/src/renderers/dom/client/eventPlugins/DefaultEventPluginOrder.js b/src/renderers/dom/client/eventPlugins/DefaultEventPluginOrder.js index a57172105..a04c61244 100644 --- a/src/renderers/dom/client/eventPlugins/DefaultEventPluginOrder.js +++ b/src/renderers/dom/client/eventPlugins/DefaultEventPluginOrder.js @@ -13,4 +13,2 @@ -var keyOf = require('keyOf'); - /** @@ -25,9 +23,9 @@ var keyOf = require('keyOf'); var DefaultEventPluginOrder = [ - keyOf({ResponderEventPlugin: null}), - keyOf({SimpleEventPlugin: null}), - keyOf({TapEventPlugin: null}), - keyOf({EnterLeaveEventPlugin: null}), - keyOf({ChangeEventPlugin: null}), - keyOf({SelectEventPlugin: null}), - keyOf({BeforeInputEventPlugin: null}), + 'ResponderEventPlugin', + 'SimpleEventPlugin', + 'TapEventPlugin', + 'EnterLeaveEventPlugin', + 'ChangeEventPlugin', + 'SelectEventPlugin', + 'BeforeInputEventPlugin', ]; diff --git a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js index d50b0d370..fa9925f14 100644 --- a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js @@ -13,3 +13,2 @@ -var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); @@ -18,20 +17,10 @@ var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var keyOf = require('keyOf'); - -var topLevelTypes = EventConstants.topLevelTypes; - var eventTypes = { mouseEnter: { - registrationName: keyOf({onMouseEnter: null}), - dependencies: [ - topLevelTypes.topMouseOut, - topLevelTypes.topMouseOver, - ], + registrationName: 'onMouseEnter', + dependencies: ['topMouseOut', 'topMouseOver'], }, mouseLeave: { - registrationName: keyOf({onMouseLeave: null}), - dependencies: [ - topLevelTypes.topMouseOut, - topLevelTypes.topMouseOver, - ], + registrationName: 'onMouseLeave', + dependencies: ['topMouseOut', 'topMouseOver'], }, @@ -40,3 +29,2 @@ var eventTypes = { var EnterLeaveEventPlugin = { - eventTypes: eventTypes, @@ -54,10 +42,11 @@ var EnterLeaveEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ) { - if (topLevelType === topLevelTypes.topMouseOver && - (nativeEvent.relatedTarget || nativeEvent.fromElement)) { + if ( + topLevelType === 'topMouseOver' && + (nativeEvent.relatedTarget || nativeEvent.fromElement) + ) { return null; } - if (topLevelType !== topLevelTypes.topMouseOut && - topLevelType !== topLevelTypes.topMouseOver) { + if (topLevelType !== 'topMouseOut' && topLevelType !== 'topMouseOver') { // Must not be a mouse in or mouse out - ignoring. @@ -82,7 +71,8 @@ var EnterLeaveEventPlugin = { var to; - if (topLevelType === topLevelTypes.topMouseOut) { + if (topLevelType === 'topMouseOut') { from = targetInst; var related = nativeEvent.relatedTarget || nativeEvent.toElement; - to = related ? - ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; + to = related + ? ReactDOMComponentTree.getClosestInstanceFromNode(related) + : null; } else { @@ -98,6 +88,8 @@ var EnterLeaveEventPlugin = { - var fromNode = - from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); - var toNode = - to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); + var fromNode = from == null + ? win + : ReactDOMComponentTree.getNodeFromInstance(from); + var toNode = to == null + ? win + : ReactDOMComponentTree.getNodeFromInstance(to); @@ -107,3 +99,3 @@ var EnterLeaveEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -117,3 +109,3 @@ var EnterLeaveEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -127,3 +119,2 @@ var EnterLeaveEventPlugin = { }, - }; diff --git a/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js b/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js index 9aa84b213..8b89dc377 100644 --- a/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SelectEventPlugin.js @@ -13,3 +13,2 @@ -var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); @@ -22,12 +21,8 @@ var getActiveElement = require('getActiveElement'); var isTextInputElement = require('isTextInputElement'); -var keyOf = require('keyOf'); var shallowEqual = require('shallowEqual'); -var topLevelTypes = EventConstants.topLevelTypes; - -var skipSelectionChangeEvent = ( +var skipSelectionChangeEvent = ExecutionEnvironment.canUseDOM && 'documentMode' in document && - document.documentMode <= 11 -); + document.documentMode <= 11; @@ -36,13 +31,14 @@ var eventTypes = { phasedRegistrationNames: { - bubbled: keyOf({onSelect: null}), - captured: keyOf({onSelectCapture: null}), + bubbled: 'onSelect', + captured: 'onSelectCapture', }, dependencies: [ - topLevelTypes.topBlur, - topLevelTypes.topContextMenu, - topLevelTypes.topFocus, - topLevelTypes.topKeyDown, - topLevelTypes.topMouseDown, - topLevelTypes.topMouseUp, - topLevelTypes.topSelectionChange, + 'topBlur', + 'topContextMenu', + 'topFocus', + 'topKeyDown', + 'topKeyUp', + 'topMouseDown', + 'topMouseUp', + 'topSelectionChange', ], @@ -59,3 +55,2 @@ var mouseDown = false; var hasListener = false; -var ON_SELECT_KEY = keyOf({onSelect: null}); @@ -71,4 +66,6 @@ var ON_SELECT_KEY = keyOf({onSelect: null}); function getSelection(node) { - if ('selectionStart' in node && - ReactInputSelection.hasSelectionCapabilities(node)) { + if ( + 'selectionStart' in node && + ReactInputSelection.hasSelectionCapabilities(node) + ) { return { @@ -107,5 +104,7 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { // won't dispatch. - if (mouseDown || - activeElement == null || - activeElement !== getActiveElement()) { + if ( + mouseDown || + activeElement == null || + activeElement !== getActiveElement() + ) { return null; @@ -122,3 +121,3 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -151,3 +150,2 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { var SelectEventPlugin = { - eventTypes: eventTypes, @@ -158,3 +156,3 @@ var SelectEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ) { @@ -164,4 +162,5 @@ var SelectEventPlugin = { - var targetNode = targetInst ? - ReactDOMComponentTree.getNodeFromInstance(targetInst) : window; + var targetNode = targetInst + ? ReactDOMComponentTree.getNodeFromInstance(targetInst) + : window; @@ -169,5 +168,7 @@ var SelectEventPlugin = { // Track the input node that has focus. - case topLevelTypes.topFocus: - if (isTextInputElement(targetNode) || - targetNode.contentEditable === 'true') { + case 'topFocus': + if ( + isTextInputElement(targetNode) || + targetNode.contentEditable === 'true' + ) { activeElement = targetNode; @@ -177,3 +178,3 @@ var SelectEventPlugin = { break; - case topLevelTypes.topBlur: + case 'topBlur': activeElement = null; @@ -182,13 +183,11 @@ var SelectEventPlugin = { break; - // Don't fire the event while the user is dragging. This matches the // semantics of the native select event. - case topLevelTypes.topMouseDown: + case 'topMouseDown': mouseDown = true; break; - case topLevelTypes.topContextMenu: - case topLevelTypes.topMouseUp: + case 'topContextMenu': + case 'topMouseUp': mouseDown = false; return constructSelectEvent(nativeEvent, nativeEventTarget); - // Chrome and IE fire non-standard event when selection is changed (and @@ -202,3 +201,3 @@ var SelectEventPlugin = { // This is also our approach for IE handling, for the reason above. - case topLevelTypes.topSelectionChange: + case 'topSelectionChange': if (skipSelectionChangeEvent) { @@ -206,5 +205,5 @@ var SelectEventPlugin = { } - // falls through - case topLevelTypes.topKeyDown: - case topLevelTypes.topKeyUp: + // falls through + case 'topKeyDown': + case 'topKeyUp': return constructSelectEvent(nativeEvent, nativeEventTarget); @@ -216,3 +215,3 @@ var SelectEventPlugin = { didPutListener: function(inst, registrationName, listener) { - if (registrationName === ON_SELECT_KEY) { + if (registrationName === 'onSelect') { hasListener = true; diff --git a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js index f790e3fe9..6acaf74a5 100644 --- a/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js @@ -9,2 +9,3 @@ * @providesModule SimpleEventPlugin + * @flow */ @@ -13,3 +14,2 @@ -var EventConstants = require('EventConstants'); var EventListener = require('EventListener'); @@ -32,457 +32,128 @@ var getEventCharCode = require('getEventCharCode'); var invariant = require('invariant'); -var keyOf = require('keyOf'); -var topLevelTypes = EventConstants.topLevelTypes; +import type {TopLevelTypes} from 'EventConstants'; +import type { + DispatchConfig, + ReactSyntheticEvent, +} from 'ReactSyntheticEventType'; +import type {ReactInstance} from 'ReactInstanceType'; +import type {EventTypes, PluginModule} from 'PluginModuleType'; -var eventTypes = { - abort: { - phasedRegistrationNames: { - bubbled: keyOf({onAbort: true}), - captured: keyOf({onAbortCapture: true}), - }, - }, - animationEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onAnimationEnd: true}), - captured: keyOf({onAnimationEndCapture: true}), - }, - }, - animationIteration: { - phasedRegistrationNames: { - bubbled: keyOf({onAnimationIteration: true}), - captured: keyOf({onAnimationIterationCapture: true}), - }, - }, - animationStart: { - phasedRegistrationNames: { - bubbled: keyOf({onAnimationStart: true}), - captured: keyOf({onAnimationStartCapture: true}), - }, - }, - blur: { - phasedRegistrationNames: { - bubbled: keyOf({onBlur: true}), - captured: keyOf({onBlurCapture: true}), - }, - }, - canPlay: { - phasedRegistrationNames: { - bubbled: keyOf({onCanPlay: true}), - captured: keyOf({onCanPlayCapture: true}), - }, - }, - canPlayThrough: { - phasedRegistrationNames: { - bubbled: keyOf({onCanPlayThrough: true}), - captured: keyOf({onCanPlayThroughCapture: true}), - }, - }, - click: { - phasedRegistrationNames: { - bubbled: keyOf({onClick: true}), - captured: keyOf({onClickCapture: true}), - }, - }, - contextMenu: { - phasedRegistrationNames: { - bubbled: keyOf({onContextMenu: true}), - captured: keyOf({onContextMenuCapture: true}), - }, - }, - copy: { - phasedRegistrationNames: { - bubbled: keyOf({onCopy: true}), - captured: keyOf({onCopyCapture: true}), - }, - }, - cut: { - phasedRegistrationNames: { - bubbled: keyOf({onCut: true}), - captured: keyOf({onCutCapture: true}), - }, - }, - doubleClick: { - phasedRegistrationNames: { - bubbled: keyOf({onDoubleClick: true}), - captured: keyOf({onDoubleClickCapture: true}), - }, - }, - drag: { - phasedRegistrationNames: { - bubbled: keyOf({onDrag: true}), - captured: keyOf({onDragCapture: true}), - }, - }, - dragEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onDragEnd: true}), - captured: keyOf({onDragEndCapture: true}), - }, - }, - dragEnter: { - phasedRegistrationNames: { - bubbled: keyOf({onDragEnter: true}), - captured: keyOf({onDragEnterCapture: true}), - }, - }, - dragExit: { - phasedRegistrationNames: { - bubbled: keyOf({onDragExit: true}), - captured: keyOf({onDragExitCapture: true}), - }, - }, - dragLeave: { - phasedRegistrationNames: { - bubbled: keyOf({onDragLeave: true}), - captured: keyOf({onDragLeaveCapture: true}), - }, - }, - dragOver: { - phasedRegistrationNames: { - bubbled: keyOf({onDragOver: true}), - captured: keyOf({onDragOverCapture: true}), - }, - }, - dragStart: { - phasedRegistrationNames: { - bubbled: keyOf({onDragStart: true}), - captured: keyOf({onDragStartCapture: true}), - }, - }, - drop: { - phasedRegistrationNames: { - bubbled: keyOf({onDrop: true}), - captured: keyOf({onDropCapture: true}), - }, - }, - durationChange: { - phasedRegistrationNames: { - bubbled: keyOf({onDurationChange: true}), - captured: keyOf({onDurationChangeCapture: true}), - }, - }, - emptied: { - phasedRegistrationNames: { - bubbled: keyOf({onEmptied: true}), - captured: keyOf({onEmptiedCapture: true}), - }, - }, - encrypted: { - phasedRegistrationNames: { - bubbled: keyOf({onEncrypted: true}), - captured: keyOf({onEncryptedCapture: true}), - }, - }, - ended: { - phasedRegistrationNames: { - bubbled: keyOf({onEnded: true}), - captured: keyOf({onEndedCapture: true}), - }, - }, - error: { - phasedRegistrationNames: { - bubbled: keyOf({onError: true}), - captured: keyOf({onErrorCapture: true}), - }, - }, - focus: { - phasedRegistrationNames: { - bubbled: keyOf({onFocus: true}), - captured: keyOf({onFocusCapture: true}), - }, - }, - input: { - phasedRegistrationNames: { - bubbled: keyOf({onInput: true}), - captured: keyOf({onInputCapture: true}), - }, - }, - invalid: { - phasedRegistrationNames: { - bubbled: keyOf({onInvalid: true}), - captured: keyOf({onInvalidCapture: true}), - }, - }, - keyDown: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyDown: true}), - captured: keyOf({onKeyDownCapture: true}), - }, - }, - keyPress: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyPress: true}), - captured: keyOf({onKeyPressCapture: true}), - }, - }, - keyUp: { - phasedRegistrationNames: { - bubbled: keyOf({onKeyUp: true}), - captured: keyOf({onKeyUpCapture: true}), - }, - }, - load: { - phasedRegistrationNames: { - bubbled: keyOf({onLoad: true}), - captured: keyOf({onLoadCapture: true}), - }, - }, - loadedData: { - phasedRegistrationNames: { - bubbled: keyOf({onLoadedData: true}), - captured: keyOf({onLoadedDataCapture: true}), - }, - }, - loadedMetadata: { - phasedRegistrationNames: { - bubbled: keyOf({onLoadedMetadata: true}), - captured: keyOf({onLoadedMetadataCapture: true}), - }, - }, - loadStart: { - phasedRegistrationNames: { - bubbled: keyOf({onLoadStart: true}), - captured: keyOf({onLoadStartCapture: true}), - }, - }, - // Note: We do not allow listening to mouseOver events. Instead, use the - // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`. - mouseDown: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseDown: true}), - captured: keyOf({onMouseDownCapture: true}), - }, - }, - mouseMove: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseMove: true}), - captured: keyOf({onMouseMoveCapture: true}), - }, - }, - mouseOut: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseOut: true}), - captured: keyOf({onMouseOutCapture: true}), - }, - }, - mouseOver: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseOver: true}), - captured: keyOf({onMouseOverCapture: true}), - }, - }, - mouseUp: { - phasedRegistrationNames: { - bubbled: keyOf({onMouseUp: true}), - captured: keyOf({onMouseUpCapture: true}), - }, - }, - paste: { - phasedRegistrationNames: { - bubbled: keyOf({onPaste: true}), - captured: keyOf({onPasteCapture: true}), - }, - }, - pause: { - phasedRegistrationNames: { - bubbled: keyOf({onPause: true}), - captured: keyOf({onPauseCapture: true}), - }, - }, - play: { - phasedRegistrationNames: { - bubbled: keyOf({onPlay: true}), - captured: keyOf({onPlayCapture: true}), - }, - }, - playing: { - phasedRegistrationNames: { - bubbled: keyOf({onPlaying: true}), - captured: keyOf({onPlayingCapture: true}), - }, - }, - progress: { - phasedRegistrationNames: { - bubbled: keyOf({onProgress: true}), - captured: keyOf({onProgressCapture: true}), - }, - }, - rateChange: { - phasedRegistrationNames: { - bubbled: keyOf({onRateChange: true}), - captured: keyOf({onRateChangeCapture: true}), - }, - }, - reset: { - phasedRegistrationNames: { - bubbled: keyOf({onReset: true}), - captured: keyOf({onResetCapture: true}), - }, - }, - scroll: { - phasedRegistrationNames: { - bubbled: keyOf({onScroll: true}), - captured: keyOf({onScrollCapture: true}), - }, - }, - seeked: { - phasedRegistrationNames: { - bubbled: keyOf({onSeeked: true}), - captured: keyOf({onSeekedCapture: true}), - }, - }, - seeking: { - phasedRegistrationNames: { - bubbled: keyOf({onSeeking: true}), - captured: keyOf({onSeekingCapture: true}), - }, - }, - stalled: { - phasedRegistrationNames: { - bubbled: keyOf({onStalled: true}), - captured: keyOf({onStalledCapture: true}), - }, - }, - submit: { - phasedRegistrationNames: { - bubbled: keyOf({onSubmit: true}), - captured: keyOf({onSubmitCapture: true}), - }, - }, - suspend: { - phasedRegistrationNames: { - bubbled: keyOf({onSuspend: true}), - captured: keyOf({onSuspendCapture: true}), - }, - }, - timeUpdate: { - phasedRegistrationNames: { - bubbled: keyOf({onTimeUpdate: true}), - captured: keyOf({onTimeUpdateCapture: true}), - }, - }, - touchCancel: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchCancel: true}), - captured: keyOf({onTouchCancelCapture: true}), - }, - }, - touchEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchEnd: true}), - captured: keyOf({onTouchEndCapture: true}), - }, - }, - touchMove: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchMove: true}), - captured: keyOf({onTouchMoveCapture: true}), - }, - }, - touchStart: { - phasedRegistrationNames: { - bubbled: keyOf({onTouchStart: true}), - captured: keyOf({onTouchStartCapture: true}), - }, - }, - transitionEnd: { - phasedRegistrationNames: { - bubbled: keyOf({onTransitionEnd: true}), - captured: keyOf({onTransitionEndCapture: true}), - }, - }, - volumeChange: { - phasedRegistrationNames: { - bubbled: keyOf({onVolumeChange: true}), - captured: keyOf({onVolumeChangeCapture: true}), - }, - }, - waiting: { - phasedRegistrationNames: { - bubbled: keyOf({onWaiting: true}), - captured: keyOf({onWaitingCapture: true}), - }, - }, - wheel: { +/** + * Turns + * ['abort', ...] + * into + * eventTypes = { + * 'abort': { + * phasedRegistrationNames: { + * bubbled: 'onAbort', + * captured: 'onAbortCapture', + * }, + * dependencies: ['topAbort'], + * }, + * ... + * }; + * topLevelEventsToDispatchConfig = { + * 'topAbort': { sameConfig } + * }; + */ +var eventTypes: EventTypes = {}; +var topLevelEventsToDispatchConfig: {[key: TopLevelTypes]: DispatchConfig} = {}; +[ + 'abort', + 'animationEnd', + 'animationIteration', + 'animationStart', + 'blur', + 'canPlay', + 'canPlayThrough', + 'click', + 'contextMenu', + 'copy', + 'cut', + 'doubleClick', + 'drag', + 'dragEnd', + 'dragEnter', + 'dragExit', + 'dragLeave', + 'dragOver', + 'dragStart', + 'drop', + 'durationChange', + 'emptied', + 'encrypted', + 'ended', + 'error', + 'focus', + 'input', + 'invalid', + 'keyDown', + 'keyPress', + 'keyUp', + 'load', + 'loadedData', + 'loadedMetadata', + 'loadStart', + 'mouseDown', + 'mouseMove', + 'mouseOut', + 'mouseOver', + 'mouseUp', + 'paste', + 'pause', + 'play', + 'playing', + 'progress', + 'rateChange', + 'reset', + 'scroll', + 'seeked', + 'seeking', + 'stalled', + 'submit', + 'suspend', + 'timeUpdate', + 'touchCancel', + 'touchEnd', + 'touchMove', + 'touchStart', + 'transitionEnd', + 'volumeChange', + 'waiting', + 'wheel', +].forEach(event => { + var capitalizedEvent = event[0].toUpperCase() + event.slice(1); + var onEvent = 'on' + capitalizedEvent; + var topEvent = 'top' + capitalizedEvent; + + var type = { phasedRegistrationNames: { - bubbled: keyOf({onWheel: true}), - captured: keyOf({onWheelCapture: true}), + bubbled: onEvent, + captured: onEvent + 'Capture', }, - }, -}; + dependencies: [topEvent], + }; + eventTypes[event] = type; + topLevelEventsToDispatchConfig[topEvent] = type; +}); -var topLevelEventsToDispatchConfig = { - topAbort: eventTypes.abort, - topAnimationEnd: eventTypes.animationEnd, - topAnimationIteration: eventTypes.animationIteration, - topAnimationStart: eventTypes.animationStart, - topBlur: eventTypes.blur, - topCanPlay: eventTypes.canPlay, - topCanPlayThrough: eventTypes.canPlayThrough, - topClick: eventTypes.click, - topContextMenu: eventTypes.contextMenu, - topCopy: eventTypes.copy, - topCut: eventTypes.cut, - topDoubleClick: eventTypes.doubleClick, - topDrag: eventTypes.drag, - topDragEnd: eventTypes.dragEnd, - topDragEnter: eventTypes.dragEnter, - topDragExit: eventTypes.dragExit, - topDragLeave: eventTypes.dragLeave, - topDragOver: eventTypes.dragOver, - topDragStart: eventTypes.dragStart, - topDrop: eventTypes.drop, - topDurationChange: eventTypes.durationChange, - topEmptied: eventTypes.emptied, - topEncrypted: eventTypes.encrypted, - topEnded: eventTypes.ended, - topError: eventTypes.error, - topFocus: eventTypes.focus, - topInput: eventTypes.input, - topInvalid: eventTypes.invalid, - topKeyDown: eventTypes.keyDown, - topKeyPress: eventTypes.keyPress, - topKeyUp: eventTypes.keyUp, - topLoad: eventTypes.load, - topLoadedData: eventTypes.loadedData, - topLoadedMetadata: eventTypes.loadedMetadata, - topLoadStart: eventTypes.loadStart, - topMouseDown: eventTypes.mouseDown, - topMouseMove: eventTypes.mouseMove, - topMouseOut: eventTypes.mouseOut, - topMouseOver: eventTypes.mouseOver, - topMouseUp: eventTypes.mouseUp, - topPaste: eventTypes.paste, - topPause: eventTypes.pause, - topPlay: eventTypes.play, - topPlaying: eventTypes.playing, - topProgress: eventTypes.progress, - topRateChange: eventTypes.rateChange, - topReset: eventTypes.reset, - topScroll: eventTypes.scroll, - topSeeked: eventTypes.seeked, - topSeeking: eventTypes.seeking, - topStalled: eventTypes.stalled, - topSubmit: eventTypes.submit, - topSuspend: eventTypes.suspend, - topTimeUpdate: eventTypes.timeUpdate, - topTouchCancel: eventTypes.touchCancel, - topTouchEnd: eventTypes.touchEnd, - topTouchMove: eventTypes.touchMove, - topTouchStart: eventTypes.touchStart, - topTransitionEnd: eventTypes.transitionEnd, - topVolumeChange: eventTypes.volumeChange, - topWaiting: eventTypes.waiting, - topWheel: eventTypes.wheel, -}; +var onClickListeners = {}; -for (var type in topLevelEventsToDispatchConfig) { - topLevelEventsToDispatchConfig[type].dependencies = [type]; +function getDictionaryKey(inst: ReactInstance): string { + // Prevents V8 performance issue: + // https://github.com/facebook/react/pull/7232 + return '.' + inst._rootNodeID; } -var ON_CLICK_KEY = keyOf({onClick: null}); -var onClickListeners = {}; - -var SimpleEventPlugin = { +function isInteractive(tag) { + return ( + tag === 'button' || + tag === 'input' || + tag === 'select' || + tag === 'textarea' + ); +} +var SimpleEventPlugin: PluginModule = { eventTypes: eventTypes, @@ -490,7 +161,7 @@ var SimpleEventPlugin = { extractEvents: function( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget - ) { + topLevelType: TopLevelTypes, + targetInst: ReactInstance, + nativeEvent: MouseEvent, + nativeEventTarget: EventTarget, + ): null | ReactSyntheticEvent { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; @@ -501,30 +172,30 @@ var SimpleEventPlugin = { switch (topLevelType) { - case topLevelTypes.topAbort: - case topLevelTypes.topCanPlay: - case topLevelTypes.topCanPlayThrough: - case topLevelTypes.topDurationChange: - case topLevelTypes.topEmptied: - case topLevelTypes.topEncrypted: - case topLevelTypes.topEnded: - case topLevelTypes.topError: - case topLevelTypes.topInput: - case topLevelTypes.topInvalid: - case topLevelTypes.topLoad: - case topLevelTypes.topLoadedData: - case topLevelTypes.topLoadedMetadata: - case topLevelTypes.topLoadStart: - case topLevelTypes.topPause: - case topLevelTypes.topPlay: - case topLevelTypes.topPlaying: - case topLevelTypes.topProgress: - case topLevelTypes.topRateChange: - case topLevelTypes.topReset: - case topLevelTypes.topSeeked: - case topLevelTypes.topSeeking: - case topLevelTypes.topStalled: - case topLevelTypes.topSubmit: - case topLevelTypes.topSuspend: - case topLevelTypes.topTimeUpdate: - case topLevelTypes.topVolumeChange: - case topLevelTypes.topWaiting: + case 'topAbort': + case 'topCanPlay': + case 'topCanPlayThrough': + case 'topDurationChange': + case 'topEmptied': + case 'topEncrypted': + case 'topEnded': + case 'topError': + case 'topInput': + case 'topInvalid': + case 'topLoad': + case 'topLoadedData': + case 'topLoadedMetadata': + case 'topLoadStart': + case 'topPause': + case 'topPlay': + case 'topPlaying': + case 'topProgress': + case 'topRateChange': + case 'topReset': + case 'topSeeked': + case 'topSeeking': + case 'topStalled': + case 'topSubmit': + case 'topSuspend': + case 'topTimeUpdate': + case 'topVolumeChange': + case 'topWaiting': // HTML Events @@ -533,3 +204,3 @@ var SimpleEventPlugin = { break; - case topLevelTypes.topKeyPress: + case 'topKeyPress': // Firefox creates a keypress event for function keys too. This removes @@ -540,12 +211,12 @@ var SimpleEventPlugin = { } - /* falls through */ - case topLevelTypes.topKeyDown: - case topLevelTypes.topKeyUp: + /* falls through */ + case 'topKeyDown': + case 'topKeyUp': EventConstructor = SyntheticKeyboardEvent; break; - case topLevelTypes.topBlur: - case topLevelTypes.topFocus: + case 'topBlur': + case 'topFocus': EventConstructor = SyntheticFocusEvent; break; - case topLevelTypes.topClick: + case 'topClick': // Firefox creates a click event on right mouse clicks. This removes the @@ -555,45 +226,47 @@ var SimpleEventPlugin = { } - /* falls through */ - case topLevelTypes.topContextMenu: - case topLevelTypes.topDoubleClick: - case topLevelTypes.topMouseDown: - case topLevelTypes.topMouseMove: - case topLevelTypes.topMouseOut: - case topLevelTypes.topMouseOver: - case topLevelTypes.topMouseUp: + /* falls through */ + case 'topDoubleClick': + case 'topMouseDown': + case 'topMouseMove': + case 'topMouseUp': + // TODO: Disabled elements should not respond to mouse events + /* falls through */ + case 'topMouseOut': + case 'topMouseOver': + case 'topContextMenu': EventConstructor = SyntheticMouseEvent; break; - case topLevelTypes.topDrag: - case topLevelTypes.topDragEnd: - case topLevelTypes.topDragEnter: - case topLevelTypes.topDragExit: - case topLevelTypes.topDragLeave: - case topLevelTypes.topDragOver: - case topLevelTypes.topDragStart: - case topLevelTypes.topDrop: + case 'topDrag': + case 'topDragEnd': + case 'topDragEnter': + case 'topDragExit': + case 'topDragLeave': + case 'topDragOver': + case 'topDragStart': + case 'topDrop': EventConstructor = SyntheticDragEvent; break; - case topLevelTypes.topTouchCancel: - case topLevelTypes.topTouchEnd: - case topLevelTypes.topTouchMove: - case topLevelTypes.topTouchStart: + case 'topTouchCancel': + case 'topTouchEnd': + case 'topTouchMove': + case 'topTouchStart': EventConstructor = SyntheticTouchEvent; break; - case topLevelTypes.topAnimationEnd: - case topLevelTypes.topAnimationIteration: - case topLevelTypes.topAnimationStart: + case 'topAnimationEnd': + case 'topAnimationIteration': + case 'topAnimationStart': EventConstructor = SyntheticAnimationEvent; break; - case topLevelTypes.topTransitionEnd: + case 'topTransitionEnd': EventConstructor = SyntheticTransitionEvent; break; - case topLevelTypes.topScroll: + case 'topScroll': EventConstructor = SyntheticUIEvent; break; - case topLevelTypes.topWheel: + case 'topWheel': EventConstructor = SyntheticWheelEvent; break; - case topLevelTypes.topCopy: - case topLevelTypes.topCut: - case topLevelTypes.topPaste: + case 'topCopy': + case 'topCut': + case 'topPaste': EventConstructor = SyntheticClipboardEvent; @@ -604,3 +277,3 @@ var SimpleEventPlugin = { 'SimpleEventPlugin: Unhandled event type, `%s`.', - topLevelType + topLevelType, ); @@ -610,3 +283,3 @@ var SimpleEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -616,3 +289,7 @@ var SimpleEventPlugin = { - didPutListener: function(inst, registrationName, listener) { + didPutListener: function( + inst: ReactInstance, + registrationName: string, + listener: () => void, + ): void { // Mobile Safari does not fire properly bubble click events on @@ -621,10 +298,11 @@ var SimpleEventPlugin = { // listener on the target node. - if (registrationName === ON_CLICK_KEY) { - var id = inst._rootNodeID; + // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html + if (registrationName === 'onClick' && !isInteractive(inst._tag)) { + var key = getDictionaryKey(inst); var node = ReactDOMComponentTree.getNodeFromInstance(inst); - if (!onClickListeners[id]) { - onClickListeners[id] = EventListener.listen( + if (!onClickListeners[key]) { + onClickListeners[key] = EventListener.listen( node, 'click', - emptyFunction + emptyFunction, ); @@ -634,10 +312,12 @@ var SimpleEventPlugin = { - willDeleteListener: function(inst, registrationName) { - if (registrationName === ON_CLICK_KEY) { - var id = inst._rootNodeID; - onClickListeners[id].remove(); - delete onClickListeners[id]; + willDeleteListener: function( + inst: ReactInstance, + registrationName: string, + ): void { + if (registrationName === 'onClick' && !isInteractive(inst._tag)) { + var key = getDictionaryKey(inst); + onClickListeners[key].remove(); + delete onClickListeners[key]; } }, - }; diff --git a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js index 670eb936a..a5b46a07e 100644 --- a/src/renderers/dom/client/eventPlugins/TapEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/TapEventPlugin.js @@ -9,2 +9,3 @@ * @providesModule TapEventPlugin + * @flow */ @@ -13,3 +14,2 @@ -var EventConstants = require('EventConstants'); var EventPluginUtils = require('EventPluginUtils'); @@ -20,5 +20,2 @@ var ViewportMetrics = require('ViewportMetrics'); -var keyOf = require('keyOf'); -var topLevelTypes = EventConstants.topLevelTypes; - var isStartish = EventPluginUtils.isStartish; @@ -26,2 +23,35 @@ var isEndish = EventPluginUtils.isEndish; +import type {EventTypes, PluginModule} from 'PluginModuleType'; +import type {ReactInstance} from 'ReactInstanceType'; +import type {TopLevelTypes} from 'EventConstants'; + +/** + * We are extending the Flow 'Touch' declaration to enable using bracket + * notation to access properties. + * Without this adjustment Flow throws + * "Indexable signature not found in Touch". + * See https://github.com/facebook/flow/issues/1323 + */ +type TouchPropertyKey = 'clientX' | 'clientY' | 'pageX' | 'pageY'; + +declare class _Touch extends Touch { + [key: TouchPropertyKey]: number, +} + +type AxisCoordinateData = { + page: TouchPropertyKey, + client: TouchPropertyKey, + envScroll: 'currentPageScrollLeft' | 'currentPageScrollTop', +}; + +type AxisType = { + x: AxisCoordinateData, + y: AxisCoordinateData, +}; + +type CoordinatesType = { + x: number, + y: number, +}; + /** @@ -31,5 +61,5 @@ var isEndish = EventPluginUtils.isEndish; var tapMoveThreshold = 10; -var startCoords = {x: null, y: null}; +var startCoords: CoordinatesType = {x: 0, y: 0}; -var Axis = { +var Axis: AxisType = { x: {page: 'pageX', client: 'clientX', envScroll: 'currentPageScrollLeft'}, @@ -38,3 +68,6 @@ var Axis = { -function getAxisCoordOfEvent(axis, nativeEvent) { +function getAxisCoordOfEvent( + axis: AxisCoordinateData, + nativeEvent: _Touch, +): number { var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent); @@ -43,8 +76,8 @@ function getAxisCoordOfEvent(axis, nativeEvent) { } - return axis.page in nativeEvent ? - nativeEvent[axis.page] : - nativeEvent[axis.client] + ViewportMetrics[axis.envScroll]; + return axis.page in nativeEvent + ? nativeEvent[axis.page] + : nativeEvent[axis.client] + ViewportMetrics[axis.envScroll]; } -function getDistance(coords, nativeEvent) { +function getDistance(coords: CoordinatesType, nativeEvent: _Touch): number { var pageX = getAxisCoordOfEvent(Axis.x, nativeEvent); @@ -53,3 +86,3 @@ function getDistance(coords, nativeEvent) { Math.pow(pageX - coords.x, 2) + Math.pow(pageY - coords.y, 2), - 0.5 + 0.5, ); @@ -58,19 +91,17 @@ function getDistance(coords, nativeEvent) { var touchEvents = [ - topLevelTypes.topTouchStart, - topLevelTypes.topTouchCancel, - topLevelTypes.topTouchEnd, - topLevelTypes.topTouchMove, + 'topTouchStart', + 'topTouchCancel', + 'topTouchEnd', + 'topTouchMove', ]; -var dependencies = [ - topLevelTypes.topMouseDown, - topLevelTypes.topMouseMove, - topLevelTypes.topMouseUp, -].concat(touchEvents); +var dependencies = ['topMouseDown', 'topMouseMove', 'topMouseUp'].concat( + touchEvents, +); -var eventTypes = { +var eventTypes: EventTypes = { touchTap: { phasedRegistrationNames: { - bubbled: keyOf({onTouchTap: null}), - captured: keyOf({onTouchTapCapture: null}), + bubbled: 'onTouchTap', + captured: 'onTouchTapCapture', }, @@ -84,4 +115,3 @@ var TOUCH_DELAY = 1000; -var TapEventPlugin = { - +var TapEventPlugin: PluginModule<_Touch> = { tapMoveThreshold: tapMoveThreshold, @@ -91,6 +121,6 @@ var TapEventPlugin = { extractEvents: function( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget + topLevelType: TopLevelTypes, + targetInst: ReactInstance, + nativeEvent: _Touch, + nativeEventTarget: EventTarget, ) { @@ -106,3 +136,3 @@ var TapEventPlugin = { } else { - if (usedTouch && (Date.now() - usedTouchTime < TOUCH_DELAY)) { + if (usedTouch && Date.now() - usedTouchTime < TOUCH_DELAY) { return null; @@ -117,3 +147,3 @@ var TapEventPlugin = { nativeEvent, - nativeEventTarget + nativeEventTarget, ); @@ -130,3 +160,2 @@ var TapEventPlugin = { }, - }; diff --git a/src/renderers/dom/client/eventPlugins/__tests__/BeforeInputEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/BeforeInputEventPlugin-test.js new file mode 100644 index 000000000..265ca820b --- /dev/null +++ b/src/renderers/dom/client/eventPlugins/__tests__/BeforeInputEventPlugin-test.js @@ -0,0 +1,240 @@ +/** + * Copyright 2013-present, 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 React = require('React'); +var ReactTestUtils = require('ReactTestUtils'); + +var EventMapping = { + compositionstart: 'topCompositionStart', + compositionend: 'topCompositionEnd', + keyup: 'topKeyUp', + keydown: 'topKeyDown', + textInput: 'topTextInput', + textinput: null, // Not defined now +}; + +describe('BeforeInputEventPlugin', function() { + var ModuleCache; + + function simulateIE11() { + document.documentMode = 11; + window.CompositionEvent = {}; + delete window.TextEvent; + } + + function simulateWebkit() { + delete document.documentMode; + window.CompositionEvent = {}; + window.TextEvent = {}; + } + + function initialize(simulator) { + // Need to delete cached modules before executing simulator + jest.resetModuleRegistry(); + + // Initialize variables in the scope of BeforeInputEventPlugin + simulator(); + + // Modules which have dependency on BeforeInputEventPlugin are stored + // in ModuleCache so that we can use these modules ouside test functions. + this.ReactDOM = require('ReactDOM'); + this.ReactDOMComponentTree = require('ReactDOMComponentTree'); + this.SyntheticCompositionEvent = require('SyntheticCompositionEvent'); + this.SyntheticInputEvent = require('SyntheticInputEvent'); + this.BeforeInputEventPlugin = require('BeforeInputEventPlugin'); + } + + function extract(node, eventType, optionalData) { + var evt = document.createEvent('HTMLEvents'); + evt.initEvent(eventType, true, true); + evt = Object.assign(evt, optionalData); + return ModuleCache.BeforeInputEventPlugin.extractEvents( + EventMapping[eventType], + ModuleCache.ReactDOMComponentTree.getInstanceFromNode(node), + evt, + node, + ); + } + + function setElementText(node) { + return args => (node.innerHTML = args); + } + + function accumulateEvents(node, events) { + // We don't use accumulateInto module to apply partial application. + return function() { + var newArgs = [node].concat(Array.prototype.slice.call(arguments)); + var newEvents = extract.apply(this, newArgs); + Array.prototype.push.apply(events, newEvents); + }; + } + + function EventMismatchError(idx, message) { + this.name = 'EventMismatchError'; + this.message = '[' + idx + '] ' + message; + } + EventMismatchError.prototype = Object.create(Error.prototype); + + function verifyEvents(actualEvents, expectedEvents) { + expect(actualEvents.length).toBe(expectedEvents.length); + expectedEvents.forEach(function(expected, idx) { + var actual = actualEvents[idx]; + expect(function() { + if (actual === null && expected.type === null) { + // Both are null. Expected. + } else if (actual === null) { + throw new EventMismatchError(idx, 'Expected not to be null'); + } else if ( + expected.type === null || + !(actual instanceof expected.type) + ) { + throw new EventMismatchError(idx, 'Unexpected type: ' + actual); + } else { + // Type match. + Object.keys(expected.data).forEach(function(expectedKey) { + if (!(expectedKey in actual)) { + throw new EventMismatchError(idx, 'KeyNotFound: ' + expectedKey); + } else if (actual[expectedKey] !== expected.data[expectedKey]) { + throw new EventMismatchError( + idx, + 'ValueMismatch: ' + actual[expectedKey], + ); + } + }); + } + }).not.toThrow(); + }); + } + + // IE fires an event named `textinput` with all lowercase characters, + // instead of a standard name `textInput`. As of now, React does not have + // a corresponding topEvent to IE's textinput, but both events are added to + // this scenario data for future use. + var Scenario_Composition = [ + {run: accumulateEvents, arg: ['compositionstart', {data: ''}]}, + {run: accumulateEvents, arg: ['textInput', {data: 'A'}]}, + {run: accumulateEvents, arg: ['textinput', {data: 'A'}]}, + {run: accumulateEvents, arg: ['keyup', {keyCode: 65}]}, + {run: setElementText, arg: ['ABC']}, + {run: accumulateEvents, arg: ['textInput', {data: 'abc'}]}, + {run: accumulateEvents, arg: ['textinput', {data: 'abc'}]}, + {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]}, + {run: setElementText, arg: ['XYZ']}, + {run: accumulateEvents, arg: ['textInput', {data: 'xyz'}]}, + {run: accumulateEvents, arg: ['textinput', {data: 'xyz'}]}, + {run: accumulateEvents, arg: ['keyup', {keyCode: 32}]}, + {run: accumulateEvents, arg: ['compositionend', {data: 'Hello'}]}, + ]; + + /* Defined expected results as a factory of result data because we need + lazy evaluation for event modules. + Event modules are reloaded to simulate a different platform per testcase. + If we define expected results as a simple dictionary here, the comparison + of 'instanceof' fails after module cache is reset. */ + + // Webkit behavior is simple. We expect SyntheticInputEvent at each + // textInput, SyntheticCompositionEvent at composition, and nothing from + // keyUp. + var Expected_Webkit = () => [ + {type: ModuleCache.SyntheticCompositionEvent, data: {}}, + {type: null}, + {type: null}, + {type: ModuleCache.SyntheticInputEvent, data: {data: 'A'}}, + {type: null}, + {type: null}, // textinput of A + {type: null}, + {type: null}, // keyUp of 65 + {type: null}, + {type: ModuleCache.SyntheticInputEvent, data: {data: 'abc'}}, + {type: null}, + {type: null}, // textinput of abc + {type: null}, + {type: null}, // keyUp of 32 + {type: null}, + {type: ModuleCache.SyntheticInputEvent, data: {data: 'xyz'}}, + {type: null}, + {type: null}, // textinput of xyz + {type: null}, + {type: null}, // keyUp of 32 + {type: ModuleCache.SyntheticCompositionEvent, data: {data: 'Hello'}}, + {type: null}, + ]; + + // For IE11, we use fallback data instead of IE's textinput events. + // We expect no SyntheticInputEvent from textinput. Fallback beforeInput is + // expected to be triggered at compositionend with a text of the target + // element, not event data. + var Expected_IE11 = () => [ + {type: ModuleCache.SyntheticCompositionEvent, data: {}}, + {type: null}, + {type: null}, + {type: null}, // textInput of A + {type: null}, + {type: null}, // textinput of A + {type: null}, + {type: null}, // keyUp of 65 + {type: null}, + {type: null}, // textInput of abc + {type: null}, + {type: null}, // textinput of abc + + // fallbackData should NOT be set at keyUp with any of END_KEYCODES + {type: null}, + {type: null}, // keyUp of 32 + + {type: null}, + {type: null}, // textInput of xyz + {type: null}, + {type: null}, // textinput of xyz + {type: null}, + {type: null}, // keyUp of 32 + + // fallbackData is retrieved from the element, which is XYZ, + // at a time of compositionend + {type: ModuleCache.SyntheticCompositionEvent, data: {}}, + {type: ModuleCache.SyntheticInputEvent, data: {data: 'XYZ'}}, + ]; + + function TestEditableReactComponent(Emulator, Scenario, ExpectedResult) { + ModuleCache = new initialize(Emulator); + + class EditableDiv extends React.Component { + render() { + return
; + } + } + var rendered = ReactTestUtils.renderIntoDocument(); + + var node = ModuleCache.ReactDOM.findDOMNode(rendered); + var events = []; + + Scenario.forEach(el => el.run.call(this, node, events).apply(this, el.arg)); + verifyEvents(events, ExpectedResult()); + } + + it('extract onBeforeInput from native textinput events', function() { + TestEditableReactComponent( + simulateWebkit, + Scenario_Composition, + Expected_Webkit, + ); + }); + + it('extract onBeforeInput from fallback objects', function() { + TestEditableReactComponent( + simulateIE11, + Scenario_Composition, + Expected_IE11, + ); + }); +}); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js index 0d29c6664..d85970cec 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/ChangeEventPlugin-test.js @@ -14,6 +14,39 @@ var React = require('React'); +var ReactDOM = require('ReactDOM'); var ReactTestUtils = require('ReactTestUtils'); +var ChangeEventPlugin = require('ChangeEventPlugin'); +var inputValueTracking = require('inputValueTracking'); -describe('ChangeEventPlugin', function() { - it('should fire change for checkbox input', function() { +function getTrackedValue(elem) { + var tracker = inputValueTracking._getTrackerFromNode(elem); + return tracker.getValue(); +} + +function setTrackedValue(elem, value) { + var tracker = inputValueTracking._getTrackerFromNode(elem); + tracker.setValue(value); +} + +function setUntrackedValue(elem, value) { + var tracker = inputValueTracking._getTrackerFromNode(elem); + var current = tracker.getValue(); + + if (elem.type === 'checkbox' || elem.type === 'radio') { + elem.checked = value; + } else { + elem.value = value; + } + tracker.setValue(current); +} + +describe('ChangeEventPlugin', () => { + beforeEach(() => { + ChangeEventPlugin._allowSimulatedPassThrough = false; + }); + + afterEach(() => { + ChangeEventPlugin._allowSimulatedPassThrough = true; + }); + + it('should fire change for checkbox input', () => { var called = 0; @@ -25,6 +58,174 @@ describe('ChangeEventPlugin', function() { - var input = ReactTestUtils.renderIntoDocument(); + var input = ReactTestUtils.renderIntoDocument( + , + ); + + setUntrackedValue(input, true); + ReactTestUtils.SimulateNative.click(input); + + expect(called).toBe(1); + }); + + it('should catch setting the value programmatically', function() { + var input = ReactTestUtils.renderIntoDocument( + , + ); + + input.value = 'bar'; + expect(getTrackedValue(input)).toBe('bar'); + }); + + it('should not fire change when setting the value programmatically', function() { + var called = 0; + + function cb(e) { + called += 1; + expect(e.type).toBe('change'); + } + + var input = ReactTestUtils.renderIntoDocument( + , + ); + + input.value = 'bar'; + ReactTestUtils.SimulateNative.change(input); + expect(called).toBe(0); + + setUntrackedValue(input, 'foo'); + ReactTestUtils.SimulateNative.change(input); + + expect(called).toBe(1); + }); + + it('should not fire change when setting checked programmatically', function() { + var called = 0; + + function cb(e) { + called += 1; + expect(e.type).toBe('change'); + } + + var input = ReactTestUtils.renderIntoDocument( + , + ); + + input.checked = true; ReactTestUtils.SimulateNative.click(input); + expect(called).toBe(0); + + input.checked = false; + setTrackedValue(input, undefined); + ReactTestUtils.SimulateNative.click(input); + expect(called).toBe(1); }); + + it('should unmount', function() { + var container = document.createElement('div'); + var input = ReactDOM.render(, container); + + ReactDOM.unmountComponentAtNode(container); + }); + + it('should only fire change for checked radio button once', function() { + var called = 0; + + function cb(e) { + called += 1; + } + + var input = ReactTestUtils.renderIntoDocument( + , + ); + setUntrackedValue(input, true); + ReactTestUtils.SimulateNative.click(input); + ReactTestUtils.SimulateNative.click(input); + expect(called).toBe(1); + }); + + it('should deduplicate input value change events', function() { + var input; + var called = 0; + + function cb(e) { + called += 1; + expect(e.type).toBe('change'); + } + + [ + , + , + , + ].forEach(function(element) { + called = 0; + input = ReactTestUtils.renderIntoDocument(element); + + setUntrackedValue(input, '40'); + ReactTestUtils.SimulateNative.change(input); + ReactTestUtils.SimulateNative.change(input); + expect(called).toBe(1); + + called = 0; + input = ReactTestUtils.renderIntoDocument(element); + setUntrackedValue(input, '40'); + ReactTestUtils.SimulateNative.input(input); + ReactTestUtils.SimulateNative.input(input); + expect(called).toBe(1); + + called = 0; + input = ReactTestUtils.renderIntoDocument(element); + setUntrackedValue(input, '40'); + ReactTestUtils.SimulateNative.input(input); + ReactTestUtils.SimulateNative.change(input); + expect(called).toBe(1); + }); + }); + + it('should listen for both change and input events when supported', function() { + var called = 0; + + function cb(e) { + called += 1; + expect(e.type).toBe('change'); + } + + if (!ChangeEventPlugin._isInputEventSupported) { + return; + } + + var input = ReactTestUtils.renderIntoDocument( + , + ); + setUntrackedValue(input, 'bar'); + + ReactTestUtils.SimulateNative.input(input); + + setUntrackedValue(input, 'foo'); + + ReactTestUtils.SimulateNative.change(input); + + expect(called).toBe(2); + }); + + it('should only fire events when the value changes for range inputs', function() { + var called = 0; + + function cb(e) { + called += 1; + expect(e.type).toBe('change'); + } + + var input = ReactTestUtils.renderIntoDocument( + , + ); + setUntrackedValue(input, '40'); + ReactTestUtils.SimulateNative.input(input); + ReactTestUtils.SimulateNative.change(input); + + setUntrackedValue(input, 'foo'); + + ReactTestUtils.SimulateNative.input(input); + ReactTestUtils.SimulateNative.change(input); + expect(called).toBe(2); + }); }); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js index 68606ead4..5fbf06893 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js @@ -14,3 +14,2 @@ var EnterLeaveEventPlugin; -var EventConstants; var React; @@ -19,6 +18,4 @@ var ReactDOMComponentTree; -var topLevelTypes; - -describe('EnterLeaveEventPlugin', function() { - beforeEach(function() { +describe('EnterLeaveEventPlugin', () => { + beforeEach(() => { jest.resetModuleRegistry(); @@ -26,3 +23,2 @@ describe('EnterLeaveEventPlugin', function() { EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); - EventConstants = require('EventConstants'); React = require('React'); @@ -30,7 +26,5 @@ describe('EnterLeaveEventPlugin', function() { ReactDOMComponentTree = require('ReactDOMComponentTree'); - - topLevelTypes = EventConstants.topLevelTypes; }); - it('should set relatedTarget properly in iframe', function() { + it('should set relatedTarget properly in iframe', () => { var iframe = document.createElement('iframe'); @@ -41,3 +35,3 @@ describe('EnterLeaveEventPlugin', function() { iframeDocument.write( - '
' + '
', ); @@ -45,3 +39,6 @@ describe('EnterLeaveEventPlugin', function() { - var component = ReactDOM.render(
, iframeDocument.body.getElementsByTagName('div')[0]); + var component = ReactDOM.render( +
, + iframeDocument.body.getElementsByTagName('div')[0], + ); var div = ReactDOM.findDOMNode(component); @@ -49,6 +46,6 @@ describe('EnterLeaveEventPlugin', function() { var extracted = EnterLeaveEventPlugin.extractEvents( - topLevelTypes.topMouseOver, + 'topMouseOver', ReactDOMComponentTree.getInstanceFromNode(div), {target: div}, - div + div, ); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/FallbackCompositionState-test.js b/src/renderers/dom/client/eventPlugins/__tests__/FallbackCompositionState-test.js index 04e09ea28..35eca38ac 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/FallbackCompositionState-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/FallbackCompositionState-test.js @@ -13,3 +13,3 @@ -describe('FallbackCompositionState', function() { +describe('FallbackCompositionState', () => { var FallbackCompositionState; @@ -18,3 +18,3 @@ describe('FallbackCompositionState', function() { - beforeEach(function() { + beforeEach(() => { FallbackCompositionState = require('FallbackCompositionState'); @@ -50,3 +50,3 @@ describe('FallbackCompositionState', function() { - it('extracts value via `getText()`', function() { + it('extracts value via `getText()`', () => { var composition = FallbackCompositionState.getPooled(getInput()); @@ -64,4 +64,4 @@ describe('FallbackCompositionState', function() { - describe('Extract fallback data inserted at collapsed cursor', function() { - it('extracts when inserted at start of text', function() { + describe('Extract fallback data inserted at collapsed cursor', () => { + it('extracts when inserted at start of text', () => { assertExtractedData('XXXHello world', 'XXX'); @@ -69,3 +69,3 @@ describe('FallbackCompositionState', function() { - it('extracts when inserted within text', function() { + it('extracts when inserted within text', () => { assertExtractedData('Hello XXXworld', 'XXX'); @@ -73,3 +73,3 @@ describe('FallbackCompositionState', function() { - it('extracts when inserted at end of text', function() { + it('extracts when inserted at end of text', () => { assertExtractedData('Hello worldXXX', 'XXX'); @@ -78,4 +78,4 @@ describe('FallbackCompositionState', function() { - describe('Extract fallback data for non-collapsed range', function() { - it('extracts when inserted at start of text', function() { + describe('Extract fallback data for non-collapsed range', () => { + it('extracts when inserted at start of text', () => { assertExtractedData('XXX world', 'XXX'); @@ -83,3 +83,3 @@ describe('FallbackCompositionState', function() { - it('extracts when inserted within text', function() { + it('extracts when inserted within text', () => { assertExtractedData('HelXXXrld', 'XXX'); @@ -87,3 +87,3 @@ describe('FallbackCompositionState', function() { - it('extracts when inserted at end of text', function() { + it('extracts when inserted at end of text', () => { assertExtractedData('Hello XXX', 'XXX'); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js index 0f032902d..41707da4f 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js @@ -13,3 +13,2 @@ -var EventConstants; var React; @@ -20,5 +19,3 @@ var SelectEventPlugin; -var topLevelTypes; - -describe('SelectEventPlugin', function() { +describe('SelectEventPlugin', () => { function extract(node, topLevelEvent) { @@ -28,3 +25,3 @@ describe('SelectEventPlugin', function() { {target: node}, - node + node, ); @@ -32,4 +29,3 @@ describe('SelectEventPlugin', function() { - beforeEach(function() { - EventConstants = require('EventConstants'); + beforeEach(() => { React = require('React'); @@ -39,12 +35,10 @@ describe('SelectEventPlugin', function() { SelectEventPlugin = require('SelectEventPlugin'); - - topLevelTypes = EventConstants.topLevelTypes; }); - it('should skip extraction if no listeners are present', function() { - var WithoutSelect = React.createClass({ - render: function() { + it('should skip extraction if no listeners are present', () => { + class WithoutSelect extends React.Component { + render() { return ; - }, - }); + } + } @@ -54,6 +48,6 @@ describe('SelectEventPlugin', function() { - var mousedown = extract(node, topLevelTypes.topMouseDown); + var mousedown = extract(node, 'topMouseDown'); expect(mousedown).toBe(null); - var mouseup = extract(node, topLevelTypes.topMouseUp); + var mouseup = extract(node, 'topMouseUp'); expect(mouseup).toBe(null); @@ -61,13 +55,13 @@ describe('SelectEventPlugin', function() { - it('should extract if an `onSelect` listener is present', function() { - var WithSelect = React.createClass({ - render: function() { + it('should extract if an `onSelect` listener is present', () => { + class WithSelect extends React.Component { + render() { return ; - }, - }); + } + } - var cb = jest.genMockFn(); + var cb = jest.fn(); var rendered = ReactTestUtils.renderIntoDocument( - + , ); @@ -79,9 +73,9 @@ describe('SelectEventPlugin', function() { - var focus = extract(node, topLevelTypes.topFocus); + var focus = extract(node, 'topFocus'); expect(focus).toBe(null); - var mousedown = extract(node, topLevelTypes.topMouseDown); + var mousedown = extract(node, 'topMouseDown'); expect(mousedown).toBe(null); - var mouseup = extract(node, topLevelTypes.topMouseUp); + var mouseup = extract(node, 'topMouseUp'); expect(mouseup).not.toBe(null); diff --git a/src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js new file mode 100644 index 000000000..09dbea866 --- /dev/null +++ b/src/renderers/dom/client/eventPlugins/__tests__/SimpleEventPlugin-test.js @@ -0,0 +1,188 @@ +/** + * Copyright 2013-present, 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'; + +describe('SimpleEventPlugin', function() { + var React; + var ReactDOM; + var ReactTestUtils; + + var onClick; + + function expectClickThru(element) { + ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element)); + expect(onClick.mock.calls.length).toBe(1); + } + + function expectNoClickThru(element) { + ReactTestUtils.SimulateNative.click(ReactDOM.findDOMNode(element)); + expect(onClick.mock.calls.length).toBe(0); + } + + function mounted(element) { + element = ReactTestUtils.renderIntoDocument(element); + return element; + } + + beforeEach(function() { + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactTestUtils = require('ReactTestUtils'); + + onClick = jest.fn(); + }); + + it('A non-interactive tags click when disabled', function() { + var element =
; + expectClickThru(mounted(element)); + }); + + it('A non-interactive tags clicks bubble when disabled', function() { + var element = ReactTestUtils.renderIntoDocument( +
, + ); + var child = ReactDOM.findDOMNode(element).firstChild; + + ReactTestUtils.SimulateNative.click(child); + expect(onClick.mock.calls.length).toBe(1); + }); + + it('does not register a click when clicking a child of a disabled element', function() { + var element = ReactTestUtils.renderIntoDocument( + , + ); + var child = ReactDOM.findDOMNode(element).querySelector('span'); + + ReactTestUtils.SimulateNative.click(child); + expect(onClick.mock.calls.length).toBe(0); + }); + + it('triggers click events for children of disabled elements', function() { + var element = ReactTestUtils.renderIntoDocument( + , + ); + var child = ReactDOM.findDOMNode(element).querySelector('span'); + + ReactTestUtils.SimulateNative.click(child); + expect(onClick.mock.calls.length).toBe(1); + }); + + it('triggers parent captured click events when target is a child of a disabled elements', function() { + var element = ReactTestUtils.renderIntoDocument( +
+ +
, + ); + var child = ReactDOM.findDOMNode(element).querySelector('span'); + + ReactTestUtils.SimulateNative.click(child); + expect(onClick.mock.calls.length).toBe(1); + }); + + it('triggers captured click events for children of disabled elements', function() { + var element = ReactTestUtils.renderIntoDocument( + , + ); + var child = ReactDOM.findDOMNode(element).querySelector('span'); + + ReactTestUtils.SimulateNative.click(child); + expect(onClick.mock.calls.length).toBe(1); + }); + + ['button', 'input', 'select', 'textarea'].forEach(function(tagName) { + describe(tagName, function() { + it('should forward clicks when it starts out not disabled', () => { + var element = React.createElement(tagName, { + onClick: onClick, + }); + + expectClickThru(mounted(element)); + }); + + it('should not forward clicks when it starts out disabled', () => { + var element = React.createElement(tagName, { + onClick: onClick, + disabled: true, + }); + + expectNoClickThru(mounted(element)); + }); + + it('should forward clicks when it becomes not disabled', () => { + var container = document.createElement('div'); + var element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick, disabled: true}), + container, + ); + element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick}), + container, + ); + expectClickThru(element); + }); + + it('should not forward clicks when it becomes disabled', () => { + var container = document.createElement('div'); + var element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick}), + container, + ); + element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick, disabled: true}), + container, + ); + expectNoClickThru(element); + }); + + it('should work correctly if the listener is changed', () => { + var container = document.createElement('div'); + var element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick, disabled: true}), + container, + ); + element = ReactDOM.render( + React.createElement(tagName, {onClick: onClick, disabled: false}), + container, + ); + expectClickThru(element); + }); + }); + }); + + describe('iOS bubbling click fix', function() { + // See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html + + it('does not add a local click to interactive elements', function() { + var container = document.createElement('div'); + + ReactDOM.render(