Skip to content

Commit

Permalink
Merge pull request #6389 from gaearon/reactperf-reactdebuginstancemap
Browse files Browse the repository at this point in the history
Add ReactDebugInstanceMap
  • Loading branch information
gaearon committed Apr 1, 2016
2 parents 599582c + 575fb79 commit a5c164d
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 0 deletions.
124 changes: 124 additions & 0 deletions src/isomorphic/ReactDebugInstanceMap.js
Original file line number Diff line number Diff line change
@@ -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;
173 changes: 173 additions & 0 deletions src/isomorphic/__tests__/ReactDebugInstanceMap-test.js
Original file line number Diff line number Diff line change
@@ -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 <div />;
}
}

spyOn(console, 'error');
var warningCount = 0;
var div = document.createElement('div');
var publicInst = ReactDOM.render(<Foo />, 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.'
);
}
});
});

0 comments on commit a5c164d

Please sign in to comment.