diff --git a/src/isomorphic/ReactDebugInstanceMap.js b/src/isomorphic/ReactDebugInstanceMap.js
deleted file mode 100644
index 50dddf4ad0e82..0000000000000
--- 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
index 24f70dd758449..deb3ba73a4ae5 100644
--- a/src/isomorphic/ReactDebugTool.js
+++ b/src/isomorphic/ReactDebugTool.js
@@ -58,17 +58,29 @@ var ReactDebugTool = {
onSetState() {
emitEvent('onSetState');
},
- onMountRootComponent(internalInstance) {
- emitEvent('onMountRootComponent', internalInstance);
+ onSetDisplayName(debugID, displayName) {
+ emitEvent('onSetDisplayName', debugID, displayName);
},
- onMountComponent(internalInstance) {
- emitEvent('onMountComponent', internalInstance);
+ onSetChildren(debugID, childDebugIDs) {
+ emitEvent('onSetChildren', debugID, childDebugIDs);
},
- onUpdateComponent(internalInstance) {
- emitEvent('onUpdateComponent', internalInstance);
+ onSetOwner(debugID, ownerDebugID) {
+ emitEvent('onSetOwner', debugID, ownerDebugID);
},
- onUnmountComponent(internalInstance) {
- emitEvent('onUnmountComponent', internalInstance);
+ onSetText(debugID, text) {
+ emitEvent('onSetText', debugID, text);
+ },
+ onMountRootComponent(debugID) {
+ emitEvent('onMountRootComponent', debugID);
+ },
+ onMountComponent(debugID) {
+ emitEvent('onMountComponent', debugID);
+ },
+ onUpdateComponent(debugID) {
+ emitEvent('onUpdateComponent', debugID);
+ },
+ onUnmountComponent(debugID) {
+ emitEvent('onUnmountComponent', debugID);
},
onTestEvent() {
emitEvent('onTestEvent');
diff --git a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js
deleted file mode 100644
index d9a063e2c649f..0000000000000
--- 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/devtools/ReactComponentTreeDevtool.js b/src/isomorphic/devtools/ReactComponentTreeDevtool.js
new file mode 100644
index 0000000000000..bdba5b378f31e
--- /dev/null
+++ b/src/isomorphic/devtools/ReactComponentTreeDevtool.js
@@ -0,0 +1,148 @@
+/**
+ * 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 ReactComponentTreeDevtool
+ */
+
+'use strict';
+
+var invariant = require('invariant');
+
+var tree = {};
+var rootIDs = [];
+
+function updateTree(id, update) {
+ if (!tree[id]) {
+ tree[id] = {
+ parentID: null,
+ ownerID: null,
+ text: null,
+ childIDs: [],
+ displayName: 'Unknown',
+ isMounted: false,
+ };
+ }
+ update(tree[id]);
+}
+
+function purgeDeep(id) {
+ var item = tree[id];
+ if (item) {
+ var {childIDs} = item;
+ delete tree[id];
+ childIDs.forEach(purgeDeep);
+ }
+}
+
+var ReactComponentTreeDevtool = {
+ onSetDisplayName(id, displayName) {
+ updateTree(id, item => item.displayName = displayName);
+ },
+
+ onSetChildren(id, nextChildIDs) {
+ updateTree(id, item => {
+ var prevChildIDs = item.childIDs;
+ item.childIDs = nextChildIDs;
+
+ nextChildIDs.forEach(nextChildID => {
+ var nextChild = tree[nextChildID];
+ invariant(
+ nextChild,
+ 'Expected devtool events to fire for the child ' +
+ 'before its parent includes it in onSetChildren().'
+ );
+ invariant(
+ nextChild.displayName != null,
+ 'Expected onSetDisplayName() to fire for the child ' +
+ 'before its parent includes it in onSetChildren().'
+ );
+ invariant(
+ nextChild.childIDs != null || nextChild.text != null,
+ 'Expected onSetChildren() or onSetText() to fire for the 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 (prevChildIDs.indexOf(nextChildID) === -1) {
+ nextChild.parentID = id;
+ }
+ });
+ });
+ },
+
+ onSetOwner(id, ownerID) {
+ updateTree(id, item => item.ownerID = ownerID);
+ },
+
+ onSetText(id, text) {
+ updateTree(id, item => item.text = text);
+ },
+
+ onMountComponent(id) {
+ updateTree(id, item => item.isMounted = true);
+ },
+
+ onMountRootComponent(id) {
+ rootIDs.push(id);
+ },
+
+ onUnmountComponent(id) {
+ updateTree(id, item => item.isMounted = false);
+ rootIDs = rootIDs.filter(rootID => rootID !== id);
+ },
+
+ purgeUnmountedComponents() {
+ Object.keys(tree)
+ .filter(id => !tree[id].isMounted)
+ .forEach(purgeDeep);
+ },
+
+ isMounted(id) {
+ var item = tree[id];
+ return item ? item.isMounted : false;
+ },
+
+ getChildIDs(id) {
+ var item = tree[id];
+ return item ? item.childIDs : [];
+ },
+
+ getDisplayName(id) {
+ var item = tree[id];
+ return item ? item.displayName : 'Unknown';
+ },
+
+ getOwnerID(id) {
+ var item = tree[id];
+ return item ? item.ownerID : null;
+ },
+
+ getParentID(id) {
+ var item = tree[id];
+ return item ? item.parentID : null;
+ },
+
+ getText(id) {
+ var item = tree[id];
+ return item ? item.text : null;
+ },
+
+ getRootIDs() {
+ return rootIDs;
+ },
+
+ getRegisteredIDs() {
+ return Object.keys(tree);
+ },
+};
+
+module.exports = ReactComponentTreeDevtool;
diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js
new file mode 100644
index 0000000000000..48f3b9d3d9f3e
--- /dev/null
+++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js
@@ -0,0 +1,1725 @@
+/**
+ * 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 ReactDebugTool;
+ var ReactDOM;
+ var ReactDOMServer;
+ var ReactInstanceMap;
+ var ReactComponentTreeDevtool;
+
+ beforeEach(() => {
+ jest.resetModuleRegistry();
+
+ React = require('React');
+ ReactDebugTool = require('ReactDebugTool');
+ ReactDOM = require('ReactDOM');
+ ReactDOMServer = require('ReactDOMServer');
+ ReactInstanceMap = require('ReactInstanceMap');
+ ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
+
+ ReactDebugTool.addDevtool(ReactComponentTreeDevtool);
+ });
+
+ afterEach(() => {
+ ReactDebugTool.removeDevtool(ReactComponentTreeDevtool);
+ });
+
+ 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 node = document.createElement('div');
+ 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];
+ }
+
+ pairs.forEach(([element, expectedTree]) => {
+ currentElement = element;
+ ReactDOM.render(, node);
+ expect(getActualTree()).toEqual(expectedTree);
+ ReactComponentTreeDevtool.purgeUnmountedComponents();
+ expect(getActualTree()).toEqual(expectedTree);
+ });
+ ReactDOM.unmountComponentAtNode(node);
+
+ ReactComponentTreeDevtool.purgeUnmountedComponents();
+ expect(getActualTree()).toBe(undefined);
+ expect(getRootDisplayNames()).toEqual([]);
+ expect(getRegisteredDisplayNames()).toEqual([]);
+
+ pairs.forEach(([element, expectedTree]) => {
+ currentElement = element;
+ ReactDOMServer.renderToString();
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ children: [{
+ displayName: 'Bar',
+ children: [],
+ }, {
+ displayName: 'Baz',
+ children: [],
+ }, {
+ displayName: 'Unknown',
+ children: [],
+ }],
+ };
+ assertTreeMatches([element, tree]);
+ });
+
+ it('reports a native tree correctly', () => {
+ var element = (
+
+ );
+ var tree = {
+ displayName: 'div',
+ children: [{
+ displayName: 'p',
+ children: [{
+ displayName: 'span',
+ children: [{
+ displayName: '#text',
+ text: 'Hi!',
+ }],
+ }, {
+ displayName: '#text',
+ text: 'Wow.',
+ }],
+ }, {
+ displayName: 'hr',
+ 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: 'div',
+ 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 (
+
+ );
+ }
+ }
+
+ var element = ;
+ var tree = {
+ displayName: 'Baz',
+ children: [{
+ displayName: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'Qux',
+ children: [],
+ }],
+ }, {
+ displayName: 'Bar',
+ children: [{
+ displayName: 'h1',
+ children: [{
+ displayName: 'span',
+ children: [{
+ displayName: '#text',
+ text: 'Hi,',
+ }],
+ }, {
+ displayName: '#text',
+ text: 'Mom',
+ }],
+ }],
+ }, {
+ displayName: 'a',
+ children: [{
+ displayName: '#text',
+ text: 'Click me.',
+ }],
+ }],
+ }],
+ };
+ 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: 'div',
+ 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: 'div',
+ children: [{
+ displayName: '#text',
+ text: '1',
+ }],
+ };
+ assertTreeMatches([element, tree]);
+ });
+
+ it('reports a single number node as a child', () => {
+ var element = {42}
;
+ var tree = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: '42',
+ }],
+ };
+ assertTreeMatches([element, tree]);
+ });
+
+ it('reports a zero as a child', () => {
+ var element = {0}
;
+ var tree = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: '0',
+ }],
+ };
+ assertTreeMatches([element, tree]);
+ });
+
+ it('skips empty nodes for multiple children', () => {
+ function Foo() {
+ return ;
+ }
+ var element = (
+
+ {'hi'}
+ {false}
+ {42}
+ {null}
+
+
+ );
+ var tree = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'hi',
+ }, {
+ displayName: '#text',
+ text: '42',
+ }, {
+ displayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ children: [],
+ }],
+ }],
+ };
+ assertTreeMatches([element, tree]);
+ });
+
+ it('reports html content as no children', () => {
+ var element = ;
+ var tree = {
+ displayName: 'div',
+ children: [],
+ };
+ assertTreeMatches([element, tree]);
+ });
+ });
+
+ describe('update', () => {
+ describe('native component', () => {
+ it('updates text of a single text child', () => {
+ var elementBefore = Hi.
;
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+
+ var elementAfter = Bye.
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [],
+ };
+
+ var elementAfter = Hi.
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from html content to a single text child', () => {
+ var elementBefore = ;
+ var treeBefore = {
+ displayName: 'div',
+ children: [],
+ };
+
+ var elementAfter = Hi.
;
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from a single text child to html content', () => {
+ var elementBefore = Hi.
;
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from no children to multiple text children', () => {
+ var elementBefore = ;
+ var treeBefore = {
+ displayName: 'div',
+ children: [],
+ };
+
+ var elementAfter = {'Hi.'}{'Bye.'}
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }, {
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from html content to multiple text children', () => {
+ var elementBefore = ;
+ var treeBefore = {
+ displayName: 'div',
+ children: [],
+ };
+
+ var elementAfter = {'Hi.'}{'Bye.'}
;
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }, {
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from multiple text children to html content', () => {
+ var elementBefore = {'Hi.'}{'Bye.'}
;
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }, {
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from html content to no children', () => {
+ var elementBefore = ;
+ var treeBefore = {
+ displayName: 'div',
+ children: [],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from no children to html content', () => {
+ var elementBefore = ;
+ var treeBefore = {
+ displayName: 'div',
+ children: [],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates from one text child to multiple text children', () => {
+ var elementBefore = Hi.
;
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+
+ var elementAfter = {'Hi.'}{'Bye.'}
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }, {
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ };
+
+ var elementAfter = Hi.
;
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates text nodes when reordering', () => {
+ var elementBefore = {'Hi.'}{'Bye.'}
;
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }, {
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ };
+
+ var elementAfter = {'Bye.'}{'Hi.'}
;
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Bye.',
+ }, {
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ };
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates native nodes when reordering with keys', () => {
+ var elementBefore = (
+
+ );
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ }, {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ }],
+ };
+
+ var elementAfter = (
+
+ );
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ }, {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ }],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates native nodes when reordering without keys', () => {
+ var elementBefore = (
+
+ );
+ var treeBefore = {
+ displayName: 'div',
+ children: [{
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ }, {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ }],
+ };
+
+ var elementAfter = (
+
+ );
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'Bye.',
+ }],
+ }, {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [],
+ }],
+ };
+
+ var elementAfter =
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ children: [],
+ }],
+ }],
+ };
+
+ var elementAfter =
;
+ var treeAfter = {
+ displayName: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'span',
+ 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: 'div',
+ children: [],
+ };
+
+ var elementAfter =
;
+ var treeAfter = {
+ displayName: 'div',
+ 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: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [],
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'div',
+ children: [],
+ };
+
+ assertTreeMatches([
+ [elementBefore, treeBefore],
+ [elementAfter, treeAfter],
+ ]);
+ });
+
+ it('updates mixed children', () => {
+ function Foo() {
+ return ;
+ }
+ var element1 = (
+
+ {'hi'}
+ {false}
+ {42}
+ {null}
+
+
+ );
+ var tree1 = {
+ displayName: 'div',
+ children: [{
+ displayName: '#text',
+ text: 'hi',
+ }, {
+ displayName: '#text',
+ text: '42',
+ }, {
+ displayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ children: [],
+ }],
+ }],
+ };
+
+ var element2 = (
+
+
+ {false}
+ {'hi'}
+ {null}
+
+ );
+ var tree2 = {
+ displayName: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ children: [],
+ }],
+ }, {
+ displayName: '#text',
+ text: 'hi',
+ }],
+ };
+
+ var element3 = (
+
+
+
+ );
+ var tree3 = {
+ displayName: 'div',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ 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: 'div',
+ children: [],
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'Foo',
+ children: [{
+ displayName: 'span',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ children: [],
+ }],
+ };
+
+ var elementAfter = ;
+ var treeAfter = {
+ displayName: 'Foo',
+ children: [{
+ displayName: 'span',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ 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: 'div',
+ 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: 'article',
+ children: [{
+ displayName: 'Foo',
+ children: [{
+ displayName: 'Bar',
+ ownerDisplayName: 'Foo',
+ children: [{
+ displayName: 'div',
+ ownerDisplayName: 'Bar',
+ children: [{
+ displayName: 'h1',
+ ownerDisplayName: 'Foo',
+ children: [{
+ displayName: '#text',
+ text: 'Hi.',
+ }],
+ }, {
+ displayName: '#text',
+ text: ' Mom',
+ }],
+ }],
+ }],
+ }],
+ };
+ assertTreeMatches([element, tree], {includeOwnerDisplayName: true});
+ });
+
+ it('preserves unmounted components until purge', () => {
+ var node = document.createElement('div');
+ 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;
+ }
+ }
+
+ ReactDOM.render(, node);
+ expect(
+ getTree(barInstance._debugID, {
+ includeParentDisplayName: true,
+ expectedParentID: fooInstance._debugID,
+ })
+ ).toEqual({
+ displayName: 'Bar',
+ parentDisplayName: 'Foo',
+ children: [],
+ });
+
+ renderBar = false;
+ ReactDOM.render(, node);
+ expect(
+ getTree(barInstance._debugID, {
+ includeParentDisplayName: true,
+ expectedParentID: fooInstance._debugID,
+ })
+ ).toEqual({
+ displayName: 'Bar',
+ parentDisplayName: 'Foo',
+ children: [],
+ });
+
+ ReactDOM.unmountComponentAtNode(node);
+ 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', () => {
+ var node = document.createElement('div');
+
+ ReactDOM.render(, node);
+ expect(getRootDisplayNames()).toEqual(['div']);
+
+ ReactDOM.render(, node);
+ expect(getRootDisplayNames()).toEqual(['div']);
+
+ ReactDOM.unmountComponentAtNode(node);
+ 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/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js
index 7f6f8cac6ff5f..1984e146f3f27 100644
--- a/src/renderers/dom/client/ReactMount.js
+++ b/src/renderers/dom/client/ReactMount.js
@@ -333,6 +333,12 @@ var ReactMount = {
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
var componentInstance = instantiateReactComponent(nextElement);
+ if (__DEV__) {
+ // Mute future events from the top level wrapper.
+ // It is an implementation detail that devtools should not know about.
+ componentInstance._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.
@@ -349,7 +355,10 @@ var ReactMount = {
instancesByReactRootID[wrapperID] = componentInstance;
if (__DEV__) {
- ReactInstrumentation.debugTool.onMountRootComponent(componentInstance);
+ // The instance here is TopLevelWrapper so we report mount for its child.
+ ReactInstrumentation.debugTool.onMountRootComponent(
+ componentInstance._renderedComponent._debugID
+ );
}
return componentInstance;
diff --git a/src/renderers/dom/server/ReactServerRendering.js b/src/renderers/dom/server/ReactServerRendering.js
index 5a5d604b7dd8d..2d30f1a18d4ae 100644
--- a/src/renderers/dom/server/ReactServerRendering.js
+++ b/src/renderers/dom/server/ReactServerRendering.js
@@ -13,7 +13,9 @@
var ReactDOMContainerInfo = require('ReactDOMContainerInfo');
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactElement = require('ReactElement');
+var ReactInstrumentation = require('ReactInstrumentation');
var ReactMarkupChecksum = require('ReactMarkupChecksum');
+var ReactReconciler = require('ReactReconciler');
var ReactServerBatchingStrategy = require('ReactServerBatchingStrategy');
var ReactServerRenderingTransaction =
require('ReactServerRenderingTransaction');
@@ -36,12 +38,18 @@ function renderToStringImpl(element, makeStaticMarkup) {
return transaction.perform(function() {
var componentInstance = instantiateReactComponent(element);
- var markup = componentInstance.mountComponent(
+ var markup = ReactReconciler.mountComponent(
+ componentInstance,
transaction,
null,
ReactDOMContainerInfo(),
emptyObject
);
+ if (__DEV__) {
+ ReactInstrumentation.debugTool.onUnmountComponent(
+ componentInstance._debugID
+ );
+ }
if (!makeStaticMarkup) {
markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
}
diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js
index 5a7cf9ff11ce2..d2c77fe1cd7b7 100644
--- a/src/renderers/dom/shared/ReactDOMComponent.js
+++ b/src/renderers/dom/shared/ReactDOMComponent.js
@@ -32,9 +32,11 @@ var ReactDOMInput = require('ReactDOMInput');
var ReactDOMOption = require('ReactDOMOption');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactDOMTextarea = require('ReactDOMTextarea');
+var ReactInstrumentation = require('ReactInstrumentation');
var ReactMultiChild = require('ReactMultiChild');
var ReactPerf = require('ReactPerf');
+var emptyFunction = require('emptyFunction');
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
@@ -245,6 +247,19 @@ function optionPostMount() {
ReactDOMOption.postMountWrapper(inst);
}
+var setContentChildForInstrumentation = emptyFunction;
+if (__DEV__) {
+ setContentChildForInstrumentation = function(contentToUse) {
+ var debugID = this._debugID;
+ var contentDebugID = debugID + '#text';
+ this._contentDebugID = contentDebugID;
+ ReactInstrumentation.debugTool.onSetDisplayName(contentDebugID, '#text');
+ ReactInstrumentation.debugTool.onSetText(contentDebugID, '' + contentToUse);
+ ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
+ ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
+ };
+}
+
// There are so many media events, it makes sense to just
// maintain a list rather than create a `trapBubbledEvent` for each
var mediaEvents = {
@@ -452,6 +467,7 @@ function ReactDOMComponent(element) {
this._flags = 0;
if (__DEV__) {
this._ancestorInfo = null;
+ this._contentDebugID = null;
}
}
@@ -717,6 +733,9 @@ ReactDOMComponent.Mixin = {
if (contentToUse != null) {
// TODO: Validate that text is allowed as a child of this node
ret = escapeTextContentForBrowser(contentToUse);
+ if (__DEV__) {
+ setContentChildForInstrumentation.call(this, contentToUse);
+ }
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
childrenToUse,
@@ -756,6 +775,9 @@ ReactDOMComponent.Mixin = {
var childrenToUse = contentToUse != null ? null : props.children;
if (contentToUse != null) {
// TODO: Validate that text is allowed as a child of this node
+ if (__DEV__) {
+ setContentChildForInstrumentation.call(this, contentToUse);
+ }
DOMLazyTree.queueText(lazyTree, contentToUse);
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
@@ -1003,17 +1025,34 @@ ReactDOMComponent.Mixin = {
this.updateChildren(null, transaction, context);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
this.updateTextContent('');
+ if (__DEV__) {
+ ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
+ }
}
if (nextContent != null) {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
+ if (__DEV__) {
+ this._contentDebugID = this._debugID + '#text';
+ setContentChildForInstrumentation.call(this, nextContent);
+ }
}
} else if (nextHtml != null) {
if (lastHtml !== nextHtml) {
this.updateMarkup('' + nextHtml);
}
+ if (__DEV__) {
+ ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
+ }
} else if (nextChildren != null) {
+ if (__DEV__) {
+ if (this._contentDebugID) {
+ ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
+ this._contentDebugID = null;
+ }
+ }
+
this.updateChildren(nextChildren, transaction, context);
}
},
@@ -1075,6 +1114,11 @@ ReactDOMComponent.Mixin = {
this._rootNodeID = null;
this._domID = null;
this._wrapperState = null;
+
+ if (this._contentDebugID) {
+ ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
+ this._contentDebugID = null;
+ }
},
getPublicInstance: function() {
diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js
index 9a69c7f97762a..5041706287355 100644
--- a/src/renderers/dom/shared/ReactDOMTextComponent.js
+++ b/src/renderers/dom/shared/ReactDOMTextComponent.js
@@ -14,6 +14,7 @@
var DOMChildrenOperations = require('DOMChildrenOperations');
var DOMLazyTree = require('DOMLazyTree');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
+var ReactInstrumentation = require('ReactInstrumentation');
var ReactPerf = require('ReactPerf');
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
@@ -67,6 +68,8 @@ Object.assign(ReactDOMTextComponent.prototype, {
context
) {
if (__DEV__) {
+ ReactInstrumentation.debugTool.onSetText(this._debugID, this._stringText);
+
var parentInfo;
if (nativeParent != null) {
parentInfo = nativeParent._ancestorInfo;
@@ -140,6 +143,10 @@ Object.assign(ReactDOMTextComponent.prototype, {
commentNodes[1],
nextStringText
);
+
+ if (__DEV__) {
+ ReactInstrumentation.debugTool.onSetText(this._debugID, nextStringText);
+ }
}
}
},
diff --git a/src/renderers/shared/reconciler/ReactCompositeComponent.js b/src/renderers/shared/reconciler/ReactCompositeComponent.js
index 907de98089ec0..26d1c8fb6e709 100644
--- a/src/renderers/shared/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/reconciler/ReactCompositeComponent.js
@@ -386,6 +386,17 @@ var ReactCompositeComponentMixin = {
this._processChildContext(context)
);
+ if (__DEV__) {
+ if (this._debugID !== 0) {
+ ReactInstrumentation.debugTool.onSetChildren(
+ this._debugID,
+ this._renderedComponent._debugID !== 0 ?
+ [this._renderedComponent._debugID] :
+ []
+ );
+ }
+ }
+
return markup;
},
@@ -853,6 +864,7 @@ var ReactCompositeComponentMixin = {
this._renderedComponent = this._instantiateReactComponent(
nextRenderedElement
);
+
var nextMarkup = ReactReconciler.mountComponent(
this._renderedComponent,
transaction,
@@ -860,6 +872,18 @@ var ReactCompositeComponentMixin = {
this._nativeContainerInfo,
this._processChildContext(context)
);
+
+ if (__DEV__) {
+ if (this._debugID !== 0) {
+ ReactInstrumentation.debugTool.onSetChildren(
+ this._debugID,
+ this._renderedComponent._debugID !== 0 ?
+ [this._renderedComponent._debugID] :
+ []
+ );
+ }
+ }
+
this._replaceNodeWithMarkup(oldNativeNode, nextMarkup);
}
},
diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js
index 66f38613218be..474ddc63dc3ef 100644
--- a/src/renderers/shared/reconciler/ReactMultiChild.js
+++ b/src/renderers/shared/reconciler/ReactMultiChild.js
@@ -12,12 +12,14 @@
'use strict';
var ReactComponentEnvironment = require('ReactComponentEnvironment');
+var ReactInstrumentation = require('ReactInstrumentation');
var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactReconciler = require('ReactReconciler');
var ReactChildReconciler = require('ReactChildReconciler');
+var emptyFunction = require('emptyFunction');
var flattenChildren = require('flattenChildren');
var invariant = require('invariant');
@@ -137,6 +139,16 @@ function processQueue(inst, updateQueue) {
);
}
+var setChildrenForInstrumentation = emptyFunction;
+if (__DEV__) {
+ setChildrenForInstrumentation = function(children) {
+ ReactInstrumentation.debugTool.onSetChildren(
+ this._debugID,
+ children ? Object.keys(children).map(key => children[key]._debugID) : []
+ );
+ };
+}
+
/**
* ReactMultiChild are capable of reconciling multiple children.
*
@@ -214,6 +226,7 @@ var ReactMultiChild = {
nestedChildren, transaction, context
);
this._renderedChildren = children;
+
var mountImages = [];
var index = 0;
for (var name in children) {
@@ -230,6 +243,11 @@ var ReactMultiChild = {
mountImages.push(mountImage);
}
}
+
+ if (__DEV__) {
+ setChildrenForInstrumentation.call(this, children);
+ }
+
return mountImages;
},
@@ -357,6 +375,10 @@ var ReactMultiChild = {
processQueue(this, updates);
}
this._renderedChildren = nextChildren;
+
+ if (__DEV__) {
+ setChildrenForInstrumentation.call(this, nextChildren);
+ }
},
/**
diff --git a/src/renderers/shared/reconciler/ReactReconciler.js b/src/renderers/shared/reconciler/ReactReconciler.js
index ae7806e4fc0ed..d2a2dbcac851e 100644
--- a/src/renderers/shared/reconciler/ReactReconciler.js
+++ b/src/renderers/shared/reconciler/ReactReconciler.js
@@ -53,7 +53,9 @@ var ReactReconciler = {
transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
}
if (__DEV__) {
- ReactInstrumentation.debugTool.onMountComponent(internalInstance);
+ if (internalInstance._debugID !== 0) {
+ ReactInstrumentation.debugTool.onMountComponent(internalInstance._debugID);
+ }
}
return markup;
},
@@ -76,7 +78,9 @@ var ReactReconciler = {
ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
internalInstance.unmountComponent(safely);
if (__DEV__) {
- ReactInstrumentation.debugTool.onUnmountComponent(internalInstance);
+ if (internalInstance._debugID !== 0) {
+ ReactInstrumentation.debugTool.onUnmountComponent(internalInstance._debugID);
+ }
}
},
@@ -128,7 +132,9 @@ var ReactReconciler = {
}
if (__DEV__) {
- ReactInstrumentation.debugTool.onUpdateComponent(internalInstance);
+ if (internalInstance._debugID !== 0) {
+ ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID);
+ }
}
},
@@ -145,7 +151,9 @@ var ReactReconciler = {
) {
internalInstance.performUpdateIfNecessary(transaction);
if (__DEV__) {
- ReactInstrumentation.debugTool.onUpdateComponent(internalInstance);
+ if (internalInstance._debugID !== 0) {
+ ReactInstrumentation.debugTool.onUpdateComponent(internalInstance._debugID);
+ }
}
},
diff --git a/src/renderers/shared/reconciler/instantiateReactComponent.js b/src/renderers/shared/reconciler/instantiateReactComponent.js
index 796e037b9958b..db22ea3b91df7 100644
--- a/src/renderers/shared/reconciler/instantiateReactComponent.js
+++ b/src/renderers/shared/reconciler/instantiateReactComponent.js
@@ -14,6 +14,7 @@
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactNativeComponent = require('ReactNativeComponent');
+var ReactInstrumentation = require('ReactInstrumentation');
var invariant = require('invariant');
var warning = require('warning');
@@ -40,6 +41,21 @@ function getDeclarationErrorAddendum(owner) {
return '';
}
+function getDisplayName(instance) {
+ var element = instance._currentElement;
+ 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 if (instance.getName) {
+ return instance.getName() || 'Unknown';
+ } else {
+ return element.type.displayName || element.type.name || 'Unknown';
+ }
+}
+
/**
* Check if the type reference is a known internal type. I.e. not a user
* provided composite type.
@@ -56,6 +72,8 @@ function isInternalComponentType(type) {
);
}
+var nextDebugID = 1;
+
/**
* Given a ReactNode, create an instance that will actually be mounted.
*
@@ -66,7 +84,8 @@ function isInternalComponentType(type) {
function instantiateReactComponent(node) {
var instance;
- if (node === null || node === false) {
+ var isEmpty = node === null || node === false;
+ if (isEmpty) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
@@ -121,6 +140,18 @@ function instantiateReactComponent(node) {
instance._warnedAboutRefsInRender = false;
}
+ if (__DEV__) {
+ var debugID = isEmpty ? 0 : nextDebugID++;
+ instance._debugID = debugID;
+
+ var displayName = getDisplayName(instance);
+ ReactInstrumentation.debugTool.onSetDisplayName(debugID, displayName);
+ var owner = node && node._owner;
+ if (owner) {
+ ReactInstrumentation.debugTool.onSetOwner(debugID, owner._debugID);
+ }
+ }
+
// Internal instances should fully constructed at this point, so they should
// not get any new fields added to them at this point.
if (__DEV__) {