From b7f6a642f61c3071b77288309548cdedab431b3b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 27 Apr 2016 22:12:31 +0100 Subject: [PATCH] Make ReactComponentTreeDevtool work with React Native --- .../ReactComponentTreeDevtool-test.js | 6 +- .../ReactComponentTreeDevtool-test.native.js | 1705 +++++++++++++++++ .../native/ReactNative/ReactNativeMount.js | 13 + .../ReactNative/ReactNativeTextComponent.js | 11 + .../native/ReactNative/__mocks__/UIManager.js | 2 + .../native/ReactNative/__mocks__/View.js | 19 + 6 files changed, 1754 insertions(+), 2 deletions(-) create mode 100644 src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js create mode 100644 src/renderers/native/ReactNative/__mocks__/View.js diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js index 7ac5be4b980..2ff1a58abb8 100644 --- a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js @@ -93,6 +93,8 @@ describe('ReactComponentTreeDevtool', () => { return getTree(rootInstance._debugID, options).children[0]; } + // Mount once, render updates, then unmount. + // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; ReactDOM.render(, node); @@ -101,17 +103,17 @@ describe('ReactComponentTreeDevtool', () => { expect(getActualTree()).toEqual(expectedTree); }); ReactDOM.unmountComponentAtNode(node); - ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); expect(getRegisteredDisplayNames()).toEqual([]); + // Server render every pair. + // Ensure the tree is correct on every step. pairs.forEach(([element, expectedTree]) => { currentElement = element; ReactDOMServer.renderToString(); expect(getActualTree()).toEqual(expectedTree); - ReactComponentTreeDevtool.purgeUnmountedComponents(); expect(getActualTree()).toBe(undefined); expect(getRootDisplayNames()).toEqual([]); diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js new file mode 100644 index 00000000000..4da93ab911a --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js @@ -0,0 +1,1705 @@ +/** + * 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('ReactComponentTreeDevtool', () => { + var React; + var ReactNative; + var ReactInstanceMap; + var ReactComponentTreeDevtool; + var createReactNativeComponentClass; + var View; + var Image; + var Text; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactNative = require('ReactNative'); + ReactInstanceMap = require('ReactInstanceMap'); + ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + View = require('View'); + createReactNativeComponentClass = require('createReactNativeComponentClass'); + Image = createReactNativeComponentClass({ + validAttributes: {}, + uiViewClassName: 'Image', + }); + var RCText = createReactNativeComponentClass({ + validAttributes: {}, + uiViewClassName: 'RCText', + }); + Text = React.createClass({ + childContextTypes: { + isInAParentText: React.PropTypes.bool, + }, + getChildContext() { + return {isInAParentText: true}; + }, + render() { + return ; + }, + }); + }); + + function getRootDisplayNames() { + return ReactComponentTreeDevtool.getRootIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getRegisteredDisplayNames() { + return ReactComponentTreeDevtool.getRegisteredIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getTree(rootID, options = {}) { + var { + includeOwnerDisplayName = false, + includeParentDisplayName = false, + expectedParentID = null, + } = options; + + var result = { + displayName: ReactComponentTreeDevtool.getDisplayName(rootID), + }; + + var ownerID = ReactComponentTreeDevtool.getOwnerID(rootID); + var parentID = ReactComponentTreeDevtool.getParentID(rootID); + expect(parentID).toBe(expectedParentID); + + if (includeParentDisplayName && parentID) { + result.parentDisplayName = ReactComponentTreeDevtool.getDisplayName(parentID); + } + if (includeOwnerDisplayName && ownerID) { + result.ownerDisplayName = ReactComponentTreeDevtool.getDisplayName(ownerID); + } + + var childIDs = ReactComponentTreeDevtool.getChildIDs(rootID); + var text = ReactComponentTreeDevtool.getText(rootID); + if (text != null) { + result.text = text; + } else { + result.children = childIDs.map(childID => + getTree(childID, {...options, expectedParentID: rootID }) + ); + } + + return result; + } + + function assertTreeMatches(pairs, options) { + if (!Array.isArray(pairs[0])) { + pairs = [pairs]; + } + + var currentElement; + var rootInstance; + + class Wrapper extends React.Component { + render() { + rootInstance = ReactInstanceMap.get(this); + return currentElement; + } + } + + function getActualTree() { + return getTree(rootInstance._debugID, options).children[0]; + } + + // Mount once, render updates, then unmount. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + ReactNative.render(, 1); + expect(getActualTree()).toEqual(expectedTree); + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toEqual(expectedTree); + }); + ReactNative.unmountComponentAtNode(1); + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + // Mount and unmount for every pair. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + ReactNative.render(, 1); + ReactNative.unmountComponentAtNode(1); + expect(getActualTree()).toEqual(expectedTree); + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); + } + + describe('mount', () => { + it('uses displayName or Unknown for classic components', () => { + var Foo = React.createClass({ + render() { + return null; + }, + }); + Foo.displayName = 'Bar'; + var Baz = React.createClass({ + render() { + return null; + }, + }); + var Qux = React.createClass({ + render() { + return null; + }, + }); + delete Qux.displayName; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or ReactComponent for modern components', () => { + class Foo extends React.Component { + render() { + return null; + } + } + Foo.displayName = 'Bar'; + class Baz extends React.Component { + render() { + return null; + } + } + class Qux extends React.Component { + render() { + return null; + } + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + // Note: Ideally fallback name should be consistent (e.g. "Unknown") + displayName: 'ReactComponent', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or Object for factory components', () => { + function Foo() { + return { + render() { + return null; + }, + }; + } + Foo.displayName = 'Bar'; + function Baz() { + return { + render() { + return null; + }, + }; + } + function Qux() { + return { + render() { + return null; + }, + }; + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or StatelessComponent for functional components', () => { + function Foo() { + return null; + } + Foo.displayName = 'Bar'; + function Baz() { + return null; + } + function Qux() { + return null; + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a native tree correctly', () => { + var element = ( + + + + Hi! + + + + + ); + var tree = { + displayName: 'View', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi!', + }], + }], + }], + }, { + displayName: 'Image', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a simple tree with composites correctly', () => { + class Foo extends React.Component { + render() { + return ; + } + } + + var element = ; + var tree = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a tree with composites correctly', () => { + var Qux = React.createClass({ + render() { + return null; + }, + }); + function Foo() { + return { + render() { + return ; + }, + }; + } + function Bar({children}) { + return {children}; + } + class Baz extends React.Component { + render() { + return ( + + + + Hi, + + + + ); + } + } + + var element = ; + var tree = { + displayName: 'Baz', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Qux', + children: [], + }], + }, { + displayName: 'Bar', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi,', + }], + }], + }], + }], + }, { + displayName: 'Image', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores null children', () => { + class Foo extends React.Component { + render() { + return null; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores false children', () => { + class Foo extends React.Component { + render() { + return false; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('reports text nodes as children', () => { + var element = {'1'}{2}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '1', + }, { + displayName: '#text', + text: '2', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single text node as a child', () => { + var element = {'1'}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '1', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single number node as a child', () => { + var element = {42}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '42', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a zero as a child', () => { + var element = {0}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '0', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('skips empty nodes for multiple children', () => { + function Foo() { + return ; + } + var element = ( + + {false} + + {null} + + + ); + var tree = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }, { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + }); + + describe('update', () => { + describe('native component', () => { + it('updates text of a single text child', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = Bye.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single text child', () => { + var elementBefore = ; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + var elementAfter = Hi.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to no children', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to multiple text children', () => { + var elementBefore = ; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + var elementAfter = {'Hi.'}{'Bye.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to no children', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from one text child to multiple text children', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = {'Hi.'}{'Bye.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to one text child', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = Hi.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates text nodes when reordering', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = {'Bye.'}{'Hi.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }, { + displayName: '#text', + text: 'Hi.', + }], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( + + Hi. + Bye. + + ); + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }], + }; + + var elementAfter = ( + + Bye. + Hi. + + ); + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( + + Hi. + Bye. + + ); + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }], + }; + + var elementAfter = ( + + Bye. + Hi. + + ); + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of a different type', () => { + function Foo() { + return null; + } + + function Bar() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of the same type', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single composite child', () => { + function Foo() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single composite child to no children', () => { + function Foo() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates mixed children', () => { + function Foo() { + return ; + } + var element1 = ( + + hi + {false} + {42} + {null} + + + ); + var tree1 = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'hi', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '42', + }], + }], + }, { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + var element2 = ( + + + {false} + hi + {null} + + ); + var tree2 = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'hi', + }], + }], + }], + }; + + var element3 = ( + + + + ); + var tree3 = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + assertTreeMatches([ + [element1, tree1], + [element2, tree2], + [element3, tree3], + ]); + }); + }); + + describe('functional component', () => { + it('updates with a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + + describe('class component', () => { + it('updates with a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + }); + + it('tracks owner correctly', () => { + class Foo extends React.Component { + render() { + return Hi.; + } + } + function Bar({children}) { + return {children}Mom; + } + + // Note that owner is not calculated for text nodes + // because they are not created from real elements. + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Bar', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'View', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'Text', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'RCText', + ownerDisplayName: 'Text', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'RCText', + ownerDisplayName: 'Text', + children: [{ + displayName: '#text', + text: 'Mom', + }], + }], + }], + }], + }], + }], + }; + assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); + }); + + it('preserves unmounted components until purge', () => { + var renderBar = true; + var fooInstance; + var barInstance; + + class Foo extends React.Component { + render() { + fooInstance = ReactInstanceMap.get(this); + return renderBar ? : null; + } + } + + class Bar extends React.Component { + render() { + barInstance = ReactInstanceMap.get(this); + return null; + } + } + + ReactNative.render(, 1); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + renderBar = false; + ReactNative.render(, 1); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + ReactNative.unmountComponentAtNode(1); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect( + getTree(barInstance._debugID, {includeParentDisplayName: true}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + }); + + it('does not report top-level wrapper as a root', () => { + ReactNative.render(, 1); + expect(getRootDisplayNames()).toEqual(['View']); + + ReactNative.render(, 1); + expect(getRootDisplayNames()).toEqual(['View']); + + ReactNative.unmountComponentAtNode(1); + expect(getRootDisplayNames()).toEqual([]); + + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getRootDisplayNames()).toEqual([]); + + // This currently contains TopLevelWrapper until purge + // so we only check it at the very end. + expect(getRegisteredDisplayNames()).toEqual([]); + }); +}); diff --git a/src/renderers/native/ReactNative/ReactNativeMount.js b/src/renderers/native/ReactNative/ReactNativeMount.js index 50d300923a2..6800b49cb9a 100644 --- a/src/renderers/native/ReactNative/ReactNativeMount.js +++ b/src/renderers/native/ReactNative/ReactNativeMount.js @@ -12,6 +12,7 @@ 'use strict'; var ReactElement = require('ReactElement'); +var ReactInstrumentation = require('ReactInstrumentation'); var ReactNativeContainerInfo = require('ReactNativeContainerInfo'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); var ReactPerf = require('ReactPerf'); @@ -138,6 +139,12 @@ var ReactNativeMount = { var instance = instantiateReactComponent(nextWrappedElement); ReactNativeMount._instancesByContainerID[containerTag] = instance; + if (__DEV__) { + // Mute future events from the top level wrapper. + // It is an implementation detail that devtools should not know about. + instance._debugID = 0; + } + // The initial render is synchronous but any updates that happen during // rendering, in componentWillMount or componentDidMount, will be batched // according to the current batching strategy. @@ -147,6 +154,12 @@ var ReactNativeMount = { instance, containerTag ); + if (__DEV__) { + // The instance here is TopLevelWrapper so we report mount for its child. + ReactInstrumentation.debugTool.onMountRootComponent( + instance._renderedComponent._debugID + ); + } var component = instance.getPublicInstance(); if (callback) { callback.call(component); diff --git a/src/renderers/native/ReactNative/ReactNativeTextComponent.js b/src/renderers/native/ReactNative/ReactNativeTextComponent.js index 3de8a82dd21..c93442c18dd 100644 --- a/src/renderers/native/ReactNative/ReactNativeTextComponent.js +++ b/src/renderers/native/ReactNative/ReactNativeTextComponent.js @@ -11,6 +11,7 @@ 'use strict'; +var ReactInstrumentation = require('ReactInstrumentation'); var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); var UIManager = require('UIManager'); @@ -28,6 +29,10 @@ var ReactNativeTextComponent = function(text) { Object.assign(ReactNativeTextComponent.prototype, { mountComponent: function(transaction, nativeParent, nativeContainerInfo, context) { + if (__DEV__) { + ReactInstrumentation.debugTool.onSetText(this._debugID, this._stringText); + } + // TODO: nativeParent should have this context already. Stop abusing context. invariant( context.isInAParentText, @@ -65,6 +70,12 @@ Object.assign(ReactNativeTextComponent.prototype, { 'RCTRawText', {text: this._stringText} ); + if (__DEV__) { + ReactInstrumentation.debugTool.onSetText( + this._debugID, + nextStringText + ); + } } } }, diff --git a/src/renderers/native/ReactNative/__mocks__/UIManager.js b/src/renderers/native/ReactNative/__mocks__/UIManager.js index 8f845f710ba..ddcba12ca03 100644 --- a/src/renderers/native/ReactNative/__mocks__/UIManager.js +++ b/src/renderers/native/ReactNative/__mocks__/UIManager.js @@ -16,6 +16,8 @@ var RCTUIManager = { setChildren: jest.fn(), manageChildren: jest.fn(), updateView: jest.fn(), + removeSubviewsFromContainerWithID: jest.fn(), + replaceExistingNonRootView: jest.fn(), }; module.exports = RCTUIManager; diff --git a/src/renderers/native/ReactNative/__mocks__/View.js b/src/renderers/native/ReactNative/__mocks__/View.js new file mode 100644 index 00000000000..71856da1673 --- /dev/null +++ b/src/renderers/native/ReactNative/__mocks__/View.js @@ -0,0 +1,19 @@ +/** + * 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 createReactNativeComponentClass = require('createReactNativeComponentClass'); + +var View = createReactNativeComponentClass({ + validAttributes: {}, + uiViewClassName: 'View', +}); + +module.exports = View;