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;