/**1* Copyright (c) 2014, Facebook, Inc. All rights reserved.2*3* This source code is licensed under the BSD-style license found in the4* LICENSE file in the root directory of this source tree. An additional grant5* of patent rights can be found in the PATENTS file in the same directory.6*/7// This module uses the Function constructor, so it can't currently run8// in strict mode9/* jshint strict:false */1011function isA(typeName, value) {12return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';13}1415function getType(ref) {16if (isA('RegExp', ref)) {17return 'regexp';18}1920if (isA('Array', ref)) {21return 'array';22}2324if (isA('Function', ref)) {25return 'function';26}2728if (isA('Object', ref)) {29return 'object';30}3132// consider number and string fields to be constants that we want to33// pick up as they are34if (isA('Number', ref) || isA('String', ref)) {35return 'constant';36}3738if (ref === undefined) {39return 'undefined';40}4142if (ref === null) {43return 'null';44}4546return null;47}4849/**50* methods on ES6 classes are not enumerable, so they can't be found with a51* simple `for (var slot in ...) {` so that had to be replaced with getSlots()52*/53function getSlots(object) {54var slots = {};55if (!object) {56return [];57}58// Simply attempting to access any of these throws an error.59var forbiddenProps = [ 'caller', 'callee', 'arguments' ];60var collectProp = function(prop) {61if (forbiddenProps.indexOf(prop) === -1) {62slots[prop] = true;63}64};65do {66Object.getOwnPropertyNames(object).forEach(collectProp);67object = Object.getPrototypeOf(object);68} while (object && Object.getPrototypeOf(object) !== null);69return Object.keys(slots);70}7172function makeComponent(metadata) {73switch (metadata.type) {74case 'object':75return {};7677case 'array':78return [];7980case 'regexp':81return new RegExp();8283case 'constant':84case 'null':85case 'undefined':86return metadata.value;8788case 'function':89var defaultReturnValue;90var specificReturnValues = [];91var mockImpl;92var isReturnValueLastSet = false;93var calls = [];94var instances = [];95var prototype =96(metadata.members && metadata.members.prototype &&97metadata.members.prototype.members) || {};9899var mockConstructor = function() {100instances.push(this);101calls.push(Array.prototype.slice.call(arguments));102if (this instanceof f) {103// This is probably being called as a constructor104getSlots(prototype).forEach(function(slot) {105// Copy prototype methods to the instance to make106// it easier to interact with mock instance call and107// return values108if (prototype[slot].type === 'function') {109var protoImpl = this[slot];110this[slot] = generateFromMetadata(prototype[slot]);111this[slot]._protoImpl = protoImpl;112}113}, this);114115// Run the mock constructor implementation116return mockImpl && mockImpl.apply(this, arguments);117}118119var returnValue;120// If return value is last set, either specific or default, i.e.121// mockReturnValueOnce()/mockReturnValue() is called and no122// mockImplementation() is called after that.123// use the set return value.124if (isReturnValueLastSet) {125returnValue = specificReturnValues.shift();126if (returnValue === undefined) {127returnValue = defaultReturnValue;128}129}130131// If mockImplementation() is last set, or specific return values132// are used up, use the mock implementation.133if (mockImpl && returnValue === undefined) {134return mockImpl.apply(this, arguments);135}136137// Otherwise use prototype implementation138if (returnValue === undefined && f._protoImpl) {139return f._protoImpl.apply(this, arguments);140}141142return returnValue;143};144145// Preserve `name` property of mocked function.146/* jshint evil:true */147var f = new Function(148'mockConstructor',149'return function ' + metadata.name + '(){' +150'return mockConstructor.apply(this,arguments);' +151'}'152)(mockConstructor);153154f._isMockFunction = true;155156f.mock = {157calls: calls,158instances: instances159};160161f.mockClear = function() {162calls.length = 0;163instances.length = 0;164};165166f.mockReturnValueOnce = function(value) {167// next function call will return this value or default return value168isReturnValueLastSet = true;169specificReturnValues.push(value);170return f;171};172173f.mockReturnValue = function(value) {174// next function call will return specified return value or this one175isReturnValueLastSet = true;176defaultReturnValue = value;177return f;178};179180f.mockImplementation = f.mockImpl = function(fn) {181// next function call will use mock implementation return value182isReturnValueLastSet = false;183mockImpl = fn;184return f;185};186187f.mockReturnThis = function() {188return f.mockImplementation(function() {189return this;190});191};192193f._getMockImplementation = function() {194return mockImpl;195};196197if (metadata.mockImpl) {198f.mockImplementation(metadata.mockImpl);199}200201return f;202}203204throw new Error('Unrecognized type ' + metadata.type);205}206207function generateFromMetadata(_metadata) {208var callbacks = [];209var refs = {};210211function generateMock(metadata) {212var mock = makeComponent(metadata);213if (metadata.refID !== null && metadata.refID !== undefined) {214refs[metadata.refID] = mock;215}216217function getRefCallback(slot, ref) {218return function() {219mock[slot] = refs[ref];220};221}222223if (metadata.__TCmeta) {224mock.__TCmeta = metadata.__TCmeta;225}226227getSlots(metadata.members).forEach(function(slot) {228var slotMetadata = metadata.members[slot];229if (slotMetadata.ref !== null && slotMetadata.ref !== undefined) {230callbacks.push(getRefCallback(slot, slotMetadata.ref));231} else {232mock[slot] = generateMock(slotMetadata);233}234});235236if (metadata.type !== 'undefined'237&& metadata.type !== 'null'238&& mock.prototype) {239mock.prototype.constructor = mock;240}241242return mock;243}244245var mock = generateMock(_metadata);246callbacks.forEach(function(setter) {247setter();248});249250return mock;251}252253function _getMetadata(component, _refs) {254var refs = _refs || [];255256// This is a potential performance drain, since the whole list is scanned257// for every component258var ref = refs.indexOf(component);259if (ref > -1) {260return {ref: ref};261}262263var type = getType(component);264if (!type) {265return null;266}267268var metadata = {type : type};269if (type === 'constant'270|| type === 'undefined'271|| type === 'null') {272metadata.value = component;273return metadata;274} else if (type === 'function') {275metadata.name = component.name;276metadata.__TCmeta = component.__TCmeta;277if (component._isMockFunction) {278metadata.mockImpl = component._getMockImplementation();279}280}281282metadata.refID = refs.length;283refs.push(component);284285var members = null;286287function addMember(slot, data) {288if (!data) {289return;290}291if (!members) {292members = {};293}294members[slot] = data;295}296297// Leave arrays alone298if (type !== 'array') {299if (type !== 'undefined') {300getSlots(component).forEach(function(slot) {301if (slot.charAt(0) === '_' ||302(type === 'function' && component._isMockFunction &&303slot.match(/^mock/))) {304return;305}306307if (!component.hasOwnProperty && component[slot] !== undefined ||308component.hasOwnProperty(slot) ||309/* jshint eqeqeq:false */310(type === 'object' && component[slot] != Object.prototype[slot])) {311addMember(slot, _getMetadata(component[slot], refs));312}313});314}315316// If component is native code function, prototype might be undefined317if (type === 'function' && component.prototype) {318var prototype = _getMetadata(component.prototype, refs);319if (prototype && prototype.members) {320addMember('prototype', prototype);321}322}323}324325if (members) {326metadata.members = members;327}328329return metadata;330}331332function removeUnusedRefs(metadata) {333function visit(metadata, f) {334f(metadata);335if (metadata.members) {336getSlots(metadata.members).forEach(function(slot) {337visit(metadata.members[slot], f);338});339}340}341342var usedRefs = {};343visit(metadata, function(metadata) {344if (metadata.ref !== null && metadata.ref !== undefined) {345usedRefs[metadata.ref] = true;346}347});348349visit(metadata, function(metadata) {350if (!usedRefs[metadata.refID]) {351delete metadata.refID;352}353});354}355356module.exports = {357/**358* Generates a mock based on the given metadata. Mocks treat functions359* specially, and all mock functions have additional members, described in the360* documentation for getMockFunction in this module.361*362* One important note: function prototoypes are handled specially by this363* mocking framework. For functions with prototypes, when called as a364* constructor, the mock will install mocked function members on the instance.365* This allows different instances of the same constructor to have different366* values for its mocks member and its return values.367*368* @param metadata Metadata for the mock in the schema returned by the369* getMetadata method of this module.370*371*/372generateFromMetadata: generateFromMetadata,373374/**375* Inspects the argument and returns its schema in the following recursive376* format:377* {378* type: ...379* members : {}380* }381*382* Where type is one of 'array', 'object', 'function', or 'ref', and members383* is an optional dictionary where the keys are member names and the values384* are metadata objects. Function prototypes are defined simply by defining385* metadata for the member.prototype of the function. The type of a function386* prototype should always be "object". For instance, a simple class might be387* defined like this:388*389* {390* type: 'function',391* members: {392* staticMethod: {type: 'function'},393* prototype: {394* type: 'object',395* members: {396* instanceMethod: {type: 'function'}397* }398* }399* }400* }401*402* Metadata may also contain references to other objects defined within the403* same metadata object. The metadata for the referent must be marked with404* 'refID' key and an arbitrary value. The referer must be marked with a405* 'ref' key that has the same value as object with refID that it refers to.406* For instance, this metadata blob:407* {408* type: 'object',409* refID: 1,410* members: {411* self: {ref: 1}412* }413* }414*415* defines an object with a slot named 'self' that refers back to the object.416*417* @param component The component for which to retrieve metadata.418*/419getMetadata: function(component) {420var metadata = _getMetadata(component);421// to make it easier to work with mock metadata, only preserve references422// that are actually used423if (metadata !== null) {424removeUnusedRefs(metadata);425}426return metadata;427},428429/**430* Generates a stand-alone function with members that help drive unit tests or431* confirm expectations. Specifically, functions returned by this method have432* the following members:433*434* .mock:435* An object with two members, "calls", and "instances", which are both436* lists. The items in the "calls" list are the arguments with which the437* function was called. The "instances" list stores the value of 'this' for438* each call to the function. This is useful for retrieving instances from a439* constructor.440*441* .mockReturnValueOnce(value)442* Pushes the given value onto a FIFO queue of return values for the443* function.444*445* .mockReturnValue(value)446* Sets the default return value for the function.447*448* .mockImplementation(function)449* Sets a mock implementation for the function.450*451* .mockReturnThis()452* Syntactic sugar for .mockImplementation(function() {return this;})453*454* In case both mockImplementation() and455* mockReturnValueOnce()/mockReturnValue() are called. The priority of456* which to use is based on what is the last call:457* - if the last call is mockReturnValueOnce() or mockReturnValue(),458* use the specific return specific return value or default return value.459* If specific return values are used up or no default return value is set,460* fall back to try mockImplementation();461* - if the last call is mockImplementation(), run the given implementation462* and return the result.463*/464getMockFunction: function() {465return makeComponent({type: 'function'});466},467468// Just a short-hand alias469getMockFn: function() {470return this.getMockFunction();471}472};473474475