From e15a481773a4ec3e1e85fc1b2e1d34dd007d4b4e Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 20 Mar 2017 19:38:54 +0000 Subject: [PATCH] Re-add tests unintentionally deleted in #9209 (#9225) These tests are useful and don't test addons specifically. I moved them to appropriate places in the codebase. --- scripts/fiber/tests-passing.txt | 13 + .../__tests__/ReactComponent-test.js | 34 ++ .../__tests__/ReactCompositeComponent-test.js | 123 +++++++ .../renderSubtreeIntoContainer-test.js | 300 ++++++++++++++++++ 4 files changed, 470 insertions(+) create mode 100644 src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 5c3d8a1f88..78b38179ba 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -466,6 +466,8 @@ src/renderers/__tests__/ReactComponent-test.js * fires the callback after a component is rendered * throws usefully when rendering badly-typed elements * includes owner name in the error about badly-typed elements +* throws if a plain object is used as a child +* throws if a plain object even if it is in an owner src/renderers/__tests__/ReactComponentLifeCycle-test.js * should not reuse an instance when it has been unmounted @@ -526,6 +528,8 @@ src/renderers/__tests__/ReactCompositeComponent-test.js * should warn when mutated props are passed * should only call componentWillUnmount once * prepares new child before unmounting old +* respects a shallow shouldComponentUpdate implementation +* does not do a deep comparison for a shallow shouldComponentUpdate implementation src/renderers/__tests__/ReactCompositeComponentDOMMinimalism-test.js * should not render extra nodes for non-interpolated text @@ -1151,6 +1155,15 @@ src/renderers/dom/shared/__tests__/quoteAttributeValueForBrowser-test.js * should escape number to string * should escape string +src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js +* should pass context when rendering subtree elsewhere +* should throw if parentComponent is invalid +* should update context if it changes due to setState +* should update context if it changes due to re-render +* should render portal with non-context-provider parent +* should get context through non-context-provider parent +* should get context through middle non-context-provider layer + src/renderers/dom/shared/__tests__/validateDOMNesting-test.js * allows any tag with no context * allows valid nestings diff --git a/src/renderers/__tests__/ReactComponent-test.js b/src/renderers/__tests__/ReactComponent-test.js index b909c8ab10..50dcec84ef 100644 --- a/src/renderers/__tests__/ReactComponent-test.js +++ b/src/renderers/__tests__/ReactComponent-test.js @@ -380,4 +380,38 @@ describe('ReactComponent', () => { // One warning for each element creation expectDev(console.error.calls.count()).toBe(1); }); + + it('throws if a plain object is used as a child', () => { + var children = { + x: , + y: , + z: , + }; + var element =
{[children]}
; + var container = document.createElement('div'); + 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.', + ); + }); + + it('throws if a plain object even if it is in an owner', () => { + class Foo extends React.Component { + render() { + var children = { + a: , + b: , + c: , + }; + return
{[children]}
; + } + } + var container = document.createElement('div'); + 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.\n\nCheck the render method of `Foo`.', + ); + }); }); diff --git a/src/renderers/__tests__/ReactCompositeComponent-test.js b/src/renderers/__tests__/ReactCompositeComponent-test.js index 6f82e6f01a..7259ec57b2 100644 --- a/src/renderers/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/__tests__/ReactCompositeComponent-test.js @@ -20,6 +20,8 @@ var ReactDOMServer; var ReactCurrentOwner; var ReactPropTypes; var ReactTestUtils; +var shallowEqual; +var shallowCompare; describe('ReactCompositeComponent', () => { beforeEach(() => { @@ -31,6 +33,12 @@ describe('ReactCompositeComponent', () => { ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); ReactPropTypes = require('ReactPropTypes'); ReactTestUtils = require('ReactTestUtils'); + shallowEqual = require('fbjs/lib/shallowEqual'); + + shallowCompare = function(instance, nextProps, nextState) { + return !shallowEqual(instance.props, nextProps) || + !shallowEqual(instance.state, nextState); + }; MorphingComponent = class extends React.Component { state = {activated: false}; @@ -1396,4 +1404,119 @@ describe('ReactCompositeComponent', () => { 'B componentDidMount', ]); }); + + it('respects a shallow shouldComponentUpdate implementation', () => { + var renderCalls = 0; + class PlasticWrap extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + color: 'green', + }; + } + + render() { + return ; + } + } + + class Apple extends React.Component { + state = { + cut: false, + slices: 1, + }; + + shouldComponentUpdate(nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + cut() { + this.setState({ + cut: true, + slices: 10, + }); + } + + eatSlice() { + this.setState({ + slices: this.state.slices - 1, + }); + } + + render() { + renderCalls++; + return
; + } + } + + var container = document.createElement('div'); + var instance = ReactDOM.render(, container); + 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 for a shallow shouldComponentUpdate implementation', () => { + function getInitialState() { + return { + foo: [1, 2, 3], + bar: {a: 4, b: 5, c: 6}, + }; + } + + var renderCalls = 0; + var initialSettings = getInitialState(); + + class Component extends React.Component { + state = initialSettings; + + shouldComponentUpdate(nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render() { + renderCalls++; + return
; + } + } + + var container = document.createElement('div'); + var instance = ReactDOM.render(, container); + 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/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js b/src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js new file mode 100644 index 0000000000..30e617480b --- /dev/null +++ b/src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js @@ -0,0 +1,300 @@ +/** + * 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 ReactDOM = require('react-dom'); +var ReactTestUtils = require('ReactTestUtils'); +var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer'); + +describe('renderSubtreeIntoContainer', () => { + it('should pass context when rendering subtree elsewhere', () => { + var portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: React.PropTypes.string.isRequired, + }; + + render() { + return
{this.context.foo}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: React.PropTypes.string.isRequired, + }; + + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return null; + } + + componentDidMount() { + expect( + function() { + renderSubtreeIntoContainer(this, , portal); + }.bind(this), + ).not.toThrow(); + } + } + + ReactTestUtils.renderIntoDocument(); + expect(portal.firstChild.innerHTML).toBe('bar'); + }); + + it('should throw if parentComponent is invalid', () => { + var portal = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: React.PropTypes.string.isRequired, + }; + + render() { + return
{this.context.foo}
; + } + } + + // 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: React.PropTypes.string.isRequired, + }; + + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return null; + } + + componentDidMount() { + expect(function() { + renderSubtreeIntoContainer(, , portal); + }).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: React.PropTypes.string.isRequired, + getFoo: React.PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: React.PropTypes.string.isRequired, + getFoo: React.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: React.PropTypes.string.isRequired, + getFoo: React.PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: React.PropTypes.string.isRequired, + getFoo: React.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'); + }); + + it('should render portal with non-context-provider parent', () => { + var container = document.createElement('div'); + document.body.appendChild(container); + var portal = document.createElement('div'); + + class Parent extends React.Component { + render() { + return null; + } + + componentDidMount() { + renderSubtreeIntoContainer(this,
hello
, portal); + } + } + + ReactDOM.render(, container); + expect(portal.firstChild.innerHTML).toBe('hello'); + }); + + it('should get context through non-context-provider parent', () => { + var container = document.createElement('div'); + document.body.appendChild(container); + var portal = document.createElement('div'); + + class Parent extends React.Component { + render() { + return ; + } + getChildContext() { + return {value: this.props.value}; + } + static childContextTypes = { + value: React.PropTypes.string.isRequired, + }; + } + + class Middle extends React.Component { + render() { + return null; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal); + } + } + + class Child extends React.Component { + static contextTypes = { + value: React.PropTypes.string.isRequired, + }; + render() { + return
{this.context.value}
; + } + } + + ReactDOM.render(, container); + expect(portal.textContent).toBe('foo'); + }); + + it('should get context through middle non-context-provider layer', () => { + var container = document.createElement('div'); + document.body.appendChild(container); + var portal1 = document.createElement('div'); + var portal2 = document.createElement('div'); + + class Parent extends React.Component { + render() { + return null; + } + getChildContext() { + return {value: this.props.value}; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal1); + } + static childContextTypes = { + value: React.PropTypes.string.isRequired, + }; + } + + class Middle extends React.Component { + render() { + return null; + } + componentDidMount() { + renderSubtreeIntoContainer(this, , portal2); + } + } + + class Child extends React.Component { + static contextTypes = { + value: React.PropTypes.string.isRequired, + }; + render() { + return
{this.context.value}
; + } + } + + ReactDOM.render(, container); + expect(portal2.textContent).toBe('foo'); + }); +});