synctos
Version:
The Syncmaker. A tool to build comprehensive sync functions for Couchbase Sync Gateway.
241 lines (206 loc) • 6.22 kB
JavaScript
/* global define */
(function (global, module) { // Browser compatibility
var simple = module.exports
var mocks = []
var totalCalls = 0
/**
* Restore the current simple and create a new one
*
* @param {Object} [obj]
* @param {String} [key]
* @api public
*/
simple.restore = function (obj, key) {
if (obj && key) {
mocks.some(function (mock, i) {
if (mock.obj !== obj || mock.key !== key) return
mock.restore()
mocks.splice(i, 1)
return true
})
return
}
mocks.forEach(_restoreMock)
mocks = []
}
/**
* Create a mocked value on an object
*
* @param {Object} obj
* @param {String} key
* @param {Function|Value} mockValue
* @return {Function} mock
* @api public
*/
simple.mock = function (obj, key, mockValue) {
if (!arguments.length) {
return simple.spyOrStub()
} else if (arguments.length === 1) {
return simple.spyOrStub(obj)
} else if (isFunction(mockValue)) {
mockValue = simple.spyOrStub(mockValue)
} else if (arguments.length === 2) {
mockValue = simple.spyOrStub(obj, key)
}
var mock = {
obj: obj,
key: key,
mockValue: mockValue,
oldValue: obj[key],
oldValueHasKey: (key in obj)
}
mock.restore = _restoreMock.bind(null, mock)
mocks.unshift(mock)
obj[key] = mockValue
return mockValue
}
/**
* Create a stub function
*
* @param {Function|Object} wrappedFn
* @param {String} [key]
* @return {Function} spy
* @api public
*/
simple.spyOrStub = simple.stub = simple.spy = function (wrappedFn, key) {
if (arguments.length === 2) {
wrappedFn = wrappedFn[key]
}
var originalFn = wrappedFn
var stubFn = function () {
var action = (newFn.loop) ? newFn.actions[(newFn.callCount - 1) % newFn.actions.length] : newFn.actions.shift()
if (!action) return
if (action.throwError) throw action.throwError
if ('returnValue' in action) return action.returnValue
if ('fn' in action) return action.fn.apply(action.ctx || this, arguments)
var cb = ('cbArgIndex' in action) ? arguments[action.cbArgIndex] : arguments[arguments.length - 1]
if (action.cbArgs) return cb.apply(action.ctx || null, action.cbArgs)
}
var newFn = function () {
var call = {
k: totalCalls++, // Keep count of calls to record the absolute order of calls
args: Array.prototype.slice.call(arguments, 0),
arg: arguments[0],
context: this
}
newFn.calls.push(call)
newFn.firstCall = newFn.firstCall || call
newFn.lastCall = call
newFn.callCount++
newFn.called = true
try {
call.returned = (wrappedFn || _noop).apply(this, arguments)
} catch(e) {
call.threw = e
throw e
}
return call.returned
}
// Spy
newFn.reset = function () {
newFn.calls = []
newFn.lastCall = { args: [] } // For dot-safety
newFn.callCount = 0
newFn.called = false
}
newFn.reset()
// Stub
newFn.actions = []
newFn.loop = true
newFn.callbackWith = newFn.callback = function () {
wrappedFn = stubFn
newFn.actions.push({ cbArgs: arguments })
return newFn // Chainable
}
newFn.callbackArgWith = newFn.callbackAtIndex = function () {
wrappedFn = stubFn
newFn.actions.push({
cbArgs: Array.prototype.slice.call(arguments, 1),
cbArgIndex: arguments[0]
})
return newFn // Chainable
}
newFn.inThisContext = function (obj) {
var action = newFn.actions[newFn.actions.length - 1]
action.ctx = obj
return newFn // Chainable
}
newFn.withArgs = function () {
var action = newFn.actions[newFn.actions.length - 1]
action.cbArgs = arguments
return newFn // Chainable
}
newFn.returnWith = function (returnValue) {
wrappedFn = stubFn
newFn.actions.push({ returnValue: returnValue })
return newFn // Chainable
}
newFn.throwWith = function (err) {
wrappedFn = stubFn
newFn.actions.push({ throwError: err })
return newFn // Chainable
}
newFn.resolveWith = function (value) {
if (simple.Promise.when) return newFn.callFn(function createResolvedPromise () { return simple.Promise.when(value) })
return newFn.callFn(function createResolvedPromise () { return simple.Promise.resolve(value) })
}
newFn.rejectWith = function (value) {
return newFn.callFn(function createRejectedPromise () { return simple.Promise.reject(value) })
}
newFn.callFn = function (fn) {
wrappedFn = stubFn
newFn.actions.push({ fn: fn })
return newFn // Chainable
}
newFn.withActions = function (actions) {
wrappedFn = stubFn
if (actions && actions.length >= 0) {
Array.prototype.push.apply(newFn.actions, actions)
}
return newFn // Chainable
}
newFn.callOriginal = newFn.callOriginalFn = function () {
wrappedFn = stubFn
newFn.actions.push({ fn: originalFn })
return newFn // Chainable
}
newFn.withLoop = function () {
newFn.loop = true
return newFn // Chainable
}
newFn.noLoop = function () {
newFn.loop = false
return newFn // Chainable
}
return newFn
}
function _restoreMock (mock) {
if (!mock.oldValueHasKey) {
delete mock.obj[mock.key]
return
}
mock.obj[mock.key] = mock.oldValue
}
function _noop () {}
/**
* Return whether the passed variable is a function
*
* @param {Mixed} value
* @return {Boolean}
* @api private
*/
function isFunction (value) {
return Object.prototype.toString.call(value) === funcClass
}
var funcClass = '[object Function]'
// Browser compatibility
if (typeof define === 'function' && define.amd) {
define(function () {
return simple
})
} else if (typeof window !== 'undefined') {
window.simple = simple
}
// Native Promise support
if (typeof Promise !== 'undefined') simple.Promise = Promise
})(this, typeof module !== 'undefined' ? module : {exports: {}})