diff --git a/src/isomorphic/ReactDebugInstanceMap.js b/src/isomorphic/ReactDebugInstanceMap.js new file mode 100644 index 0000000000000..50dddf4ad0e82 --- /dev/null +++ b/src/isomorphic/ReactDebugInstanceMap.js @@ -0,0 +1,124 @@ +/** + * 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/__tests__/ReactDebugInstanceMap-test.js b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js new file mode 100644 index 0000000000000..d9a063e2c649f --- /dev/null +++ b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js @@ -0,0 +1,173 @@ +/** + * 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.' + ); + } + }); +});