Files
react/src/test/mocks.js
T
Ben Newman 2d61639f90 Store dirtyMocks array globally so it can survive dumpCache().
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.
2013-07-15 19:41:40 -04:00

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'});
}
};