mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
2d61639f90
When require("mock-modules").dumpCache() is called, all mock functions
previously created continue to refer to the old dirtyMocks array.
If we replace that array with a new one, those mock functions will never
have their .mockClear() methods called again.
The upstream version of mocks.js pulls a similar global trick, and I never
understood why until now.
433 lines
12 KiB
JavaScript
433 lines
12 KiB
JavaScript
/**
|
|
* Copyright 2013 Facebook, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
* @providesModule mocks
|
|
*/
|
|
|
|
function isA(typeName, value) {
|
|
return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
|
|
}
|
|
|
|
function getType(ref) {
|
|
if (isA('RegExp', ref)) {
|
|
return 'regexp';
|
|
}
|
|
|
|
if (isA('Array', ref)) {
|
|
return 'array';
|
|
}
|
|
|
|
if (isA('Function', ref)) {
|
|
return 'function';
|
|
}
|
|
|
|
if (isA('Object', ref)) {
|
|
return 'object';
|
|
}
|
|
|
|
// consider number and string fields to be constants that we want to
|
|
// pick up as they are
|
|
if (isA('Number', ref) || isA('String', ref)) {
|
|
return 'constant';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function makeComponent(metadata) {
|
|
switch (metadata.type) {
|
|
case 'object':
|
|
return {};
|
|
|
|
case 'array':
|
|
return [];
|
|
|
|
case 'regexp':
|
|
return new RegExp();
|
|
|
|
case 'constant':
|
|
return metadata.value;
|
|
|
|
case 'function':
|
|
var defaultReturnValue;
|
|
var specificReturnValues = [];
|
|
var mockImpl;
|
|
var isReturnValueLastSet = false;
|
|
var calls = [];
|
|
var instances = [];
|
|
var prototype =
|
|
(metadata.members && metadata.members.prototype &&
|
|
metadata.members.prototype.members) || {};
|
|
|
|
var f = function() {
|
|
global.dirtyMocks.push(f);
|
|
|
|
instances.push(this);
|
|
calls.push(Array.prototype.slice.call(arguments));
|
|
if (this instanceof arguments.callee) {
|
|
// This is probably being called as a constructor
|
|
for (var slot in prototype) {
|
|
// Copy prototype methods to the instance to make
|
|
// it easier to interact with mock instance call and
|
|
// return values
|
|
if (prototype[slot].type == 'function') {
|
|
var protoImpl = this[slot];
|
|
this[slot] = generateFromMetadata(prototype[slot]);
|
|
this[slot]._protoImpl = protoImpl;
|
|
}
|
|
}
|
|
|
|
// Run the mock constructor implementation
|
|
mockImpl && mockImpl.apply(this, arguments);
|
|
return;
|
|
}
|
|
|
|
var returnValue;
|
|
// If return value is last set, either specific or default, i.e.
|
|
// mockReturnValue()/mockDefaultReturnValue() is called and no
|
|
// mockImplementation() is called after that.
|
|
// use the set return value.
|
|
if (isReturnValueLastSet) {
|
|
returnValue = specificReturnValues.shift();
|
|
if (returnValue === undefined) {
|
|
returnValue = defaultReturnValue;
|
|
}
|
|
}
|
|
|
|
// If mockImplementation() is last set, or specific return values
|
|
// are used up, use the mock implementation.
|
|
if (mockImpl && returnValue === undefined) {
|
|
return mockImpl.apply(this, arguments);
|
|
}
|
|
|
|
// Otherwise use prototype implementation
|
|
if (returnValue === undefined && arguments.callee._protoImpl) {
|
|
return arguments.callee._protoImpl.apply(this, arguments);
|
|
}
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
f._isMockFunction = true;
|
|
|
|
f.mock = {
|
|
calls : calls,
|
|
instances : instances
|
|
};
|
|
|
|
f.mockClear = function() {
|
|
calls.length = 0;
|
|
instances.length = 0;
|
|
};
|
|
|
|
f.mockReturnValue = function(value) {
|
|
// next function call will return this value or default return value
|
|
isReturnValueLastSet = true;
|
|
specificReturnValues.push(value);
|
|
return f;
|
|
};
|
|
|
|
f.mockDefaultReturnValue = function(value) {
|
|
// next function call will return specified return value or this one
|
|
isReturnValueLastSet = true;
|
|
defaultReturnValue = value;
|
|
return f;
|
|
};
|
|
|
|
f.mockImplementation = function(fn) {
|
|
// next function call will use mock implementation return value
|
|
isReturnValueLastSet = false;
|
|
mockImpl = fn;
|
|
return f;
|
|
};
|
|
|
|
f.mockReturnThis = function() {
|
|
return f.mockImplementation(function() {
|
|
return this;
|
|
});
|
|
};
|
|
|
|
f._getMockImplementation = function() {
|
|
return mockImpl;
|
|
};
|
|
|
|
if (metadata.mockImpl) {
|
|
f.mockImplementation(metadata.mockImpl);
|
|
}
|
|
|
|
return f;
|
|
}
|
|
|
|
throw new Error('Unrecognized type ' + metadata.type);
|
|
}
|
|
|
|
function generateFromMetadata(_metadata) {
|
|
var callbacks = [];
|
|
var refs = {};
|
|
|
|
function generateMock(metadata) {
|
|
var mock = makeComponent(metadata);
|
|
if (metadata.ref_id != null) {
|
|
refs[metadata.ref_id] = mock;
|
|
}
|
|
|
|
function getRefCallback(slot, ref) {
|
|
return function() {
|
|
mock[slot] = refs[ref];
|
|
};
|
|
}
|
|
|
|
for (var slot in metadata.members) {
|
|
var slotMetadata = metadata.members[slot];
|
|
if (slotMetadata.ref != null) {
|
|
callbacks.push(getRefCallback(slot, slotMetadata.ref));
|
|
} else {
|
|
mock[slot] = generateMock(slotMetadata);
|
|
}
|
|
}
|
|
|
|
return mock;
|
|
}
|
|
|
|
var mock = generateMock(_metadata);
|
|
callbacks.forEach(function(setter) {
|
|
setter();
|
|
});
|
|
|
|
return mock;
|
|
}
|
|
|
|
|
|
function _getMetadata(component, _refs) {
|
|
var refs = _refs || [];
|
|
|
|
// This is a potential performance drain, since the whole list is scanned
|
|
// for every component
|
|
var ref = refs.indexOf(component);
|
|
if (ref > -1) {
|
|
return {ref: ref};
|
|
}
|
|
|
|
var type = getType(component);
|
|
if (!type) {
|
|
return null;
|
|
}
|
|
|
|
var metadata = {type : type};
|
|
if (type == 'constant') {
|
|
metadata.value = component;
|
|
return metadata;
|
|
} else if (type == 'function') {
|
|
if (component._isMockFunction) {
|
|
metadata.mockImpl = component._getMockImplementation();
|
|
}
|
|
}
|
|
|
|
metadata.ref_id = refs.length;
|
|
refs.push(component);
|
|
|
|
var members = null;
|
|
|
|
function addMember(slot, data) {
|
|
if (!data) {
|
|
return;
|
|
}
|
|
if (!members) {
|
|
members = {};
|
|
}
|
|
members[slot] = data;
|
|
}
|
|
|
|
// Leave arrays alone
|
|
if (type != 'array') {
|
|
for (var slot in component) {
|
|
if (slot.charAt(0) == '_' ||
|
|
(type == 'function' && component._isMockFunction &&
|
|
slot.match(/^mock/))) {
|
|
continue;
|
|
}
|
|
|
|
if (component.hasOwnProperty(slot) ||
|
|
(type == 'object' && component[slot] != Object.prototype[slot])) {
|
|
addMember(slot, _getMetadata(component[slot], refs));
|
|
}
|
|
}
|
|
|
|
// If component is native code function, prototype might be undefined
|
|
if (type == 'function' && component.prototype) {
|
|
var prototype = _getMetadata(component.prototype, refs);
|
|
if (prototype && prototype.members) {
|
|
addMember('prototype', prototype);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (members) {
|
|
metadata.members = members;
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
function removeUnusedRefs(metadata) {
|
|
function visit(md, f) {
|
|
f(md);
|
|
if (md.members) {
|
|
for (var slot in md.members) {
|
|
visit(md.members[slot], f);
|
|
}
|
|
}
|
|
}
|
|
|
|
var usedRefs = {};
|
|
visit(metadata, function(md) {
|
|
if (md.ref != null) {
|
|
usedRefs[md.ref] = true;
|
|
}
|
|
});
|
|
|
|
visit(metadata, function(md) {
|
|
if (!usedRefs[md.ref_id]) {
|
|
delete md.ref_id;
|
|
}
|
|
});
|
|
}
|
|
|
|
var global = Function("return this")();
|
|
global.dirtyMocks = global.dirtyMocks || [];
|
|
|
|
module.exports = {
|
|
/**
|
|
* Invokes the .mockClear method of all function mocks that have been
|
|
* called since the last time clear was called.
|
|
*/
|
|
clear: function() {
|
|
var old = global.dirtyMocks;
|
|
global.dirtyMocks = [];
|
|
old.forEach(function(mock) {
|
|
mock.mockClear();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Generates a mock based on the given metadata. Mocks treat functions
|
|
* specially, and all mock functions have additional members, described in the
|
|
* documentation for getMockFunction in this module.
|
|
*
|
|
* One important note: function prototoypes are handled specially by this
|
|
* mocking framework. For functions with prototypes, when called as a
|
|
* constructor, the mock will install mocked function members on the instance.
|
|
* This allows different instances of the same constructor to have different
|
|
* values for its mocks member and its return values.
|
|
*
|
|
* @param metadata Metadata for the mock in the schema returned by the
|
|
* getMetadata method of this module.
|
|
*
|
|
*/
|
|
generateFromMetadata: generateFromMetadata,
|
|
|
|
/**
|
|
* Inspects the argument and returns its schema in the following recursive
|
|
* format:
|
|
* {
|
|
* type: ...
|
|
* members : {}
|
|
* }
|
|
*
|
|
* Where type is one of 'array', 'object', 'function', or 'ref', and members
|
|
* is an optional dictionary where the keys are member names and the values
|
|
* are metadata objects. Function prototypes are defined simply by defining
|
|
* metadata for the member.prototype of the function. The type of a function
|
|
* prototype should always be "object". For instance, a simple class might be
|
|
* defined like this:
|
|
*
|
|
* {
|
|
* type: 'function',
|
|
* members: {
|
|
* staticMethod: {type: 'function'},
|
|
* prototype: {
|
|
* type: 'object',
|
|
* members: {
|
|
* instanceMethod: {type: 'function'}
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* Metadata may also contain references to other objects defined within the
|
|
* same metadata object. The metadata for the referent must be marked with
|
|
* 'ref_id' key and an arbitrary value. The referer must be marked with a
|
|
* 'ref' key that has the same value as object with ref_id that it refers to.
|
|
* For instance, this metadata blob:
|
|
* {
|
|
* type: 'object',
|
|
* ref_id: 1,
|
|
* members: {
|
|
* self: {ref: 1}
|
|
* }
|
|
* }
|
|
*
|
|
* defines an object with a slot named 'self' that refers back to the object.
|
|
*
|
|
* @param component The component for which to retrieve metadata.
|
|
*/
|
|
getMetadata: function(component) {
|
|
var metadata = _getMetadata(component);
|
|
// to make it easier to work with mock metadata, only preserve references
|
|
// that are actually used
|
|
removeUnusedRefs(metadata);
|
|
return metadata;
|
|
},
|
|
|
|
/**
|
|
* Generates a stand-alone function with members that help drive unit tests or
|
|
* confirm expectations. Specifically, functions returned by this method have
|
|
* the following members:
|
|
*
|
|
* .mock:
|
|
* An object with two members, "calls", and "instances", which are both
|
|
* lists. The items in the "calls" list are the arguments with which the
|
|
* function was called. The "instances" list stores the value of 'this' for
|
|
* each call to the function. This is useful for retrieving instances from a
|
|
* constructor.
|
|
*
|
|
* .mockReturnValue(value)
|
|
* Pushes the given value onto a FIFO queue of return values for the
|
|
* function.
|
|
*
|
|
* .mockDefaultReturnValue(value)
|
|
* Sets the default return value for the function.
|
|
*
|
|
* .mockImplementation(function)
|
|
* Sets a mock implementation for the function.
|
|
*
|
|
* .mockReturnThis()
|
|
* Syntactic sugar for .mockImplementation(function() {return this;})
|
|
*
|
|
* In case both mockImplementation() and
|
|
* mockReturnValue()/mockDefaultReturnValue() are called. The priority of
|
|
* which to use is based on what is the last call:
|
|
* - if the last call is mockRetrunValue() or mockDefaultReturnValue(),
|
|
* use the specific return specific return value or default return value.
|
|
* If specific return values are used up or no default return value is set,
|
|
* fall back to try mockImplementation();
|
|
* - if the last call is mockImplementation(), run the given implementation
|
|
* and return the result.
|
|
*/
|
|
getMockFunction: function() {
|
|
return makeComponent({type: 'function'});
|
|
}
|
|
};
|