silverstripe-framework/thirdparty/jquery-concrete/vendor/jspec/lib/jspec.js
Ingo Schommer dca8c0cb6f merging
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@92557 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-11-21 02:33:06 +00:00

1715 lines
49 KiB
JavaScript

// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
(function(){
JSpec = {
version : '2.8.4',
cache : {},
suites : [],
modules : [],
allSuites : [],
matchers : {},
stubbed : [],
request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
stats : { specs : 0, assertions : 0, failures : 0, passes : 0, specsFinished : 0, suitesFinished : 0 },
options : { profile : false },
/**
* Default context in which bodies are evaluated.
*
* Replace context simply by setting JSpec.context
* to your own like below:
*
* JSpec.context = { foo : 'bar' }
*
* Contexts can be changed within any body, this can be useful
* in order to provide specific helper methods to specific suites.
*
* To reset (usually in after hook) simply set to null like below:
*
* JSpec.context = null
*
*/
defaultContext : {
/**
* Return an object used for proxy assertions.
* This object is used to indicate that an object
* should be an instance of _object_, not the constructor
* itself.
*
* @param {function} constructor
* @return {hash}
* @api public
*/
an_instance_of : function(constructor) {
return { an_instance_of : constructor }
},
/**
* Load fixture at _path_. This utility function
* supplies the means to resolve, and cache fixture contents
* via the DOM or Rhino.
*
* Fixtures are resolved as:
*
* - <path>
* - fixtures/<path>
* - fixtures/<path>.html
*
* @param {string} path
* @return {string}
* @api public
*/
fixture : function(path) {
if (JSpec.cache[path]) return JSpec.cache[path]
return JSpec.cache[path] =
JSpec.tryLoading(path) ||
JSpec.tryLoading('fixtures/' + path) ||
JSpec.tryLoading('fixtures/' + path + '.html') ||
JSpec.tryLoading('spec/' + path) ||
JSpec.tryLoading('spec/fixtures/' + path) ||
JSpec.tryLoading('spec/fixtures/' + path + '.html')
}
},
// --- Objects
formatters : {
/**
* Default formatter, outputting to the DOM.
*
* Options:
* - reportToId id of element to output reports to, defaults to 'jspec'
* - failuresOnly displays only suites with failing specs
*
* @api public
*/
DOM : function(results, options) {
var id = option('reportToId') || 'jspec'
var report = document.getElementById(id)
var failuresOnly = option('failuresOnly')
var classes = results.stats.failures ? 'has-failures' : ''
if (!report) throw 'JSpec requires the element #' + id + ' to output its reports'
var markup =
'<div id="jspec-report" class="' + classes + '"><div class="heading"> \
<span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
<span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
</div><table class="suites">'
bodyContents = function(body) {
return JSpec.
escape(JSpec.contentsOf(body)).
replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
replace("\n", '<br/>')
}
renderSuite = function(suite) {
var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
if (displaySuite && suite.hasSpecs()) {
markup += '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>'
each(suite.specs, function(i, spec){
markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
if (spec.requiresImplementation())
markup += '<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>'
else if (spec.passed() && !failuresOnly)
markup += '<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>'
else if(!spec.passed())
markup += '<td class="fail">' + escape(spec.description) + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
markup += '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
})
markup += '</tr>'
}
}
renderSuites = function(suites) {
each(suites, function(suite){
renderSuite(suite)
if (suite.hasSuites()) renderSuites(suite.suites)
})
}
renderSuites(results.suites)
markup += '</table></div>'
report.innerHTML = markup
},
/**
* Terminal formatter.
*
* @api public
*/
Terminal : function(results, options) {
failuresOnly = option('failuresOnly')
print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n")
indent = function(string) {
return string.replace(/^(.)/gm, ' $1')
}
renderSuite = function(suite) {
displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
if (displaySuite && suite.hasSpecs()) {
print(color(' ' + suite.description, 'bold'))
each(suite.specs, function(spec){
var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
return graph + color('.', assertion.passed ? 'green' : 'red')
})
if (spec.requiresImplementation())
print(color(' ' + spec.description, 'blue') + assertionsGraph)
else if (spec.passed() && !failuresOnly)
print(color(' ' + spec.description, 'green') + assertionsGraph)
else if (!spec.passed())
print(color(' ' + spec.description, 'red') + assertionsGraph +
"\n" + indent(spec.failure().message) + "\n")
})
print("")
}
}
renderSuites = function(suites) {
each(suites, function(suite){
renderSuite(suite)
if (suite.hasSuites()) renderSuites(suite.suites)
})
}
renderSuites(results.suites)
},
/**
* Console formatter, tested with Firebug and Safari 4.
*
* @api public
*/
Console : function(results, options) {
console.log('')
console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
renderSuite = function(suite) {
if (suite.ran) {
console.group(suite.description)
each(suite.specs, function(spec){
var assertionCount = spec.assertions.length + ':'
if (spec.requiresImplementation())
console.warn(spec.description)
else if (spec.passed())
console.log(assertionCount + ' ' + spec.description)
else
console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
})
console.groupEnd()
}
}
renderSuites = function(suites) {
each(suites, function(suite){
renderSuite(suite)
if (suite.hasSuites()) renderSuites(suite.suites)
})
}
renderSuites(results.suites)
}
},
Assertion : function(matcher, actual, expected, negate) {
extend(this, {
message : '',
passed : false,
actual : actual,
negate : negate,
matcher : matcher,
expected : expected,
// Report assertion results
report : function() {
this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
return this
},
// Run the assertion
run : function() {
// TODO: remove unshifting
expected.unshift(actual)
this.result = matcher.match.apply(this, expected)
this.passed = negate ? !this.result : this.result
if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name)
return this
}
})
},
ProxyAssertion : function(object, method, times, negate) {
var self = this
var old = object[method]
// Proxy
object[method] = function(){
args = argumentsToArray(arguments)
result = old.apply(object, args)
self.calls.push({ args : args, result : result })
return result
}
// Times
this.times = {
'once' : 1,
'twice' : 2
}[times] || times || 1
extend(this, {
calls : [],
message : '',
defer : true,
passed : false,
negate : negate,
object : object,
method : method,
// Proxy return value
and_return : function(result) {
this.expectedResult = result
return this
},
// Proxy arguments passed
with_args : function() {
this.expectedArgs = argumentsToArray(arguments)
return this
},
// Check if any calls have failing results
anyResultsFail : function() {
return any(this.calls, function(call){
return self.expectedResult.an_instance_of ?
call.result.constructor != self.expectedResult.an_instance_of:
hash(self.expectedResult) != hash(call.result)
})
},
// Check if any calls have passing results
anyResultsPass : function() {
return any(this.calls, function(call){
return self.expectedResult.an_instance_of ?
call.result.constructor == self.expectedResult.an_instance_of:
hash(self.expectedResult) == hash(call.result)
})
},
// Return the passing result
passingResult : function() {
return this.anyResultsPass().result
},
// Return the failing result
failingResult : function() {
return this.anyResultsFail().result
},
// Check if any arguments fail
anyArgsFail : function() {
return any(this.calls, function(call){
return any(self.expectedArgs, function(i, arg){
if (arg == null) return call.args[i] == null
return arg.an_instance_of ?
call.args[i].constructor != arg.an_instance_of:
hash(arg) != hash(call.args[i])
})
})
},
// Check if any arguments pass
anyArgsPass : function() {
return any(this.calls, function(call){
return any(self.expectedArgs, function(i, arg){
return arg.an_instance_of ?
call.args[i].constructor == arg.an_instance_of:
hash(arg) == hash(call.args[i])
})
})
},
// Return the passing args
passingArgs : function() {
return this.anyArgsPass().args
},
// Return the failing args
failingArgs : function() {
return this.anyArgsFail().args
},
// Report assertion results
report : function() {
this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++
return this
},
// Run the assertion
run : function() {
var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' )
function times(n) {
return n > 2 ? n + ' times' : { 1 : 'once', 2 : 'twice' }[n]
}
if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail()))
this.message = methodString + ' to return ' + puts(this.expectedResult) +
' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult()))
if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail()))
this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) +
' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs()))
if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length == this.times : this.calls.length != this.times)
this.message = methodString + ' to be called ' + times(this.times) +
', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length))
if (!this.message.length)
this.passed = true
return this
}
})
},
/**
* Specification Suite block object.
*
* @param {string} description
* @param {function} body
* @api private
*/
Suite : function(description, body) {
var self = this
extend(this, {
body: body,
description: description,
suites: [],
specs: [],
ran: false,
hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
// Add a spec to the suite
addSpec : function(description, body) {
var spec = new JSpec.Spec(description, body)
this.specs.push(spec)
JSpec.stats.specs++ // TODO: abstract
spec.suite = this
},
// Add a hook to the suite
addHook : function(hook, body) {
this.hooks[hook].push(body)
},
// Add a nested suite
addSuite : function(description, body) {
var suite = new JSpec.Suite(description, body)
JSpec.allSuites.push(suite)
suite.name = suite.description
suite.description = this.description + ' ' + suite.description
this.suites.push(suite)
suite.suite = this
},
// Invoke a hook in context to this suite
hook : function(hook) {
if (this.suite) this.suite.hook(hook)
each(this.hooks[hook], function(body) {
JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ")
})
},
// Check if nested suites are present
hasSuites : function() {
return this.suites.length
},
// Check if this suite has specs
hasSpecs : function() {
return this.specs.length
},
// Check if the entire suite passed
passed : function() {
return !any(this.specs, function(spec){
return !spec.passed()
})
}
})
},
/**
* Specification block object.
*
* @param {string} description
* @param {function} body
* @api private
*/
Spec : function(description, body) {
extend(this, {
body : body,
description : description,
assertions : [],
// Add passing assertion
pass : function(message) {
this.assertions.push({ passed : true, message : message })
++JSpec.stats.passes
},
// Add failing assertion
fail : function(message) {
this.assertions.push({ passed : false, message : message })
++JSpec.stats.failures
},
// Run deferred assertions
runDeferredAssertions : function() {
each(this.assertions, function(assertion){
if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion)
})
},
// Find first failing assertion
failure : function() {
return find(this.assertions, function(assertion){
return !assertion.passed
})
},
// Find all failing assertions
failures : function() {
return select(this.assertions, function(assertion){
return !assertion.passed
})
},
// Weither or not the spec passed
passed : function() {
return !this.failure()
},
// Weither or not the spec requires implementation (no assertions)
requiresImplementation : function() {
return this.assertions.length == 0
},
// Sprite based assertions graph
assertionsGraph : function() {
return map(this.assertions, function(assertion){
return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
}).join('')
}
})
},
Module : function(methods) {
extend(this, methods)
},
// --- DSLs
DSLs : {
snake : {
expect : function(actual){
return JSpec.expect(actual)
},
describe : function(description, body) {
return JSpec.currentSuite.addSuite(description, body)
},
it : function(description, body) {
return JSpec.currentSuite.addSpec(description, body)
},
before : function(body) {
return JSpec.currentSuite.addHook('before', body)
},
after : function(body) {
return JSpec.currentSuite.addHook('after', body)
},
before_each : function(body) {
return JSpec.currentSuite.addHook('before_each', body)
},
after_each : function(body) {
return JSpec.currentSuite.addHook('after_each', body)
},
should_behave_like : function(description) {
return JSpec.shareBehaviorsOf(description)
}
}
},
// --- Methods
/**
* Check if _value_ is 'stop'. For use as a
* utility callback function.
*
* @param {mixed} value
* @return {bool}
* @api public
*/
haveStopped : function(value) {
return value === 'stop'
},
/**
* Include _object_ which may be a hash or Module instance.
*
* @param {has, Module} object
* @return {JSpec}
* @api public
*/
include : function(object) {
var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object)
this.modules.push(module)
if ('init' in module) module.init()
if ('utilities' in module) extend(this.defaultContext, module.utilities)
if ('matchers' in module) this.addMatchers(module.matchers)
if ('DSLs' in module)
each(module.DSLs, function(name, methods){
JSpec.DSLs[name] = JSpec.DSLs[name] || {}
extend(JSpec.DSLs[name], methods)
})
return this
},
/**
* Add a module hook _name_, which is immediately
* called per module with the _args_ given. An array of
* hook return values is returned.
*
* @param {name} string
* @param {...} args
* @return {array}
* @api private
*/
hook : function(name, args) {
args = argumentsToArray(arguments, 1)
return inject(JSpec.modules, [], function(results, module){
if (typeof module[name] == 'function')
results.push(JSpec.evalHook(module, name, args))
})
},
/**
* Eval _module_ hook _name_ with _args_. Evaluates in context
* to the module itself, JSpec, and JSpec.context.
*
* @param {Module} module
* @param {string} name
* @param {array} args
* @return {mixed}
* @api private
*/
evalHook : function(module, name, args) {
var context = this.context || this.defaultContext
var contents = this.contentsOf(module[name])
var params = this.paramsFor(module[name])
module.utilities = module.utilities || {}
params.unshift('context'); args.unshift(context)
hook('evaluatingHookBody', module, name, context)
try { return new Function(params.join(), 'with (this.utilities) { with (context) { with (JSpec) { ' + contents + ' }}}').apply(module, args) }
catch(e) { error('Error in hook ' + module.name + "." + name + ': ', e) }
},
/**
* Same as hook() however accepts only one _arg_ which is
* considered immutable. This function passes the arg
* to the first module, then passes the return value of the last
* module called, to the following module.
*
* @param {string} name
* @param {mixed} arg
* @return {mixed}
* @api private
*/
hookImmutable : function(name, arg) {
return inject(JSpec.modules, arg, function(result, module){
if (typeof module[name] == 'function')
return JSpec.evalHook(module, name, [result])
})
},
/**
* Find a suite by its description or name.
*
* @param {string} description
* @return {Suite}
* @api private
*/
findSuite : function(description) {
return find(this.allSuites, function(suite){
return suite.name == description || suite.description == description
})
},
/**
* Share behaviors (specs) of the given suite with
* the current suite.
*
* @param {string} description
* @api public
*/
shareBehaviorsOf : function(description) {
if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite)
else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name'
},
/**
* Copy specs from one suite to another.
*
* @param {Suite} fromSuite
* @param {Suite} toSuite
* @api public
*/
copySpecs : function(fromSuite, toSuite) {
each(fromSuite.specs, function(spec){
spec.assertions = []
toSuite.specs.push(spec)
})
},
/**
* Convert arguments to an array.
*
* @param {object} arguments
* @param {int} offset
* @return {array}
* @api public
*/
argumentsToArray : function(arguments, offset) {
return Array.prototype.slice.call(arguments, offset || 0)
},
/**
* Return ANSI-escaped colored string.
*
* @param {string} string
* @param {string} color
* @return {string}
* @api public
*/
color : function(string, color) {
return "\u001B[" + {
bold : 1,
black : 30,
red : 31,
green : 32,
yellow : 33,
blue : 34,
magenta : 35,
cyan : 36,
white : 37
}[color] + 'm' + string + "\u001B[0m"
},
/**
* Default matcher message callback.
*
* @api private
*/
defaultMatcherMessage : function(actual, expected, negate, name) {
return 'expected ' + puts(actual) + ' to ' +
(negate ? 'not ' : '') +
name.replace(/_/g, ' ') +
' ' + puts.apply(this, expected.slice(1))
},
/**
* Normalize a matcher message.
*
* When no messge callback is present the defaultMatcherMessage
* will be assigned, will suffice for most matchers.
*
* @param {hash} matcher
* @return {hash}
* @api public
*/
normalizeMatcherMessage : function(matcher) {
if (typeof matcher.message != 'function')
matcher.message = this.defaultMatcherMessage
return matcher
},
/**
* Normalize a matcher body
*
* This process allows the following conversions until
* the matcher is in its final normalized hash state.
*
* - '==' becomes 'actual == expected'
* - 'actual == expected' becomes 'return actual == expected'
* - function(actual, expected) { return actual == expected } becomes
* { match : function(actual, expected) { return actual == expected }}
*
* @param {mixed} body
* @return {hash}
* @api public
*/
normalizeMatcherBody : function(body) {
switch (body.constructor) {
case String:
if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
if (body.length < 4) body = 'actual ' + body + ' expected'
return { match : function(actual, expected) { return eval(body) }}
case Function:
return { match : body }
default:
return body
}
},
/**
* Get option value. This method first checks if
* the option key has been set via the query string,
* otherwise returning the options hash value.
*
* @param {string} key
* @return {mixed}
* @api public
*/
option : function(key) {
return (value = query(key)) !== null ? value :
JSpec.options[key] || null
},
/**
* Generates a hash of the object passed.
*
* @param {object} object
* @return {string}
* @api private
*/
hash : function(object) {
if (object == null) return 'null'
if (object == undefined) return 'undefined'
serialize = function(prefix) {
return inject(object, prefix + ':', function(buffer, key, value){
return buffer += hash(value)
})
}
switch (object.constructor) {
case Array : return serialize('a')
case RegExp: return 'r:' + object.toString()
case Number: return 'n:' + object.toString()
case String: return 's:' + object.toString()
case Object: return 'o:' + inject(object, [], function(array, key, value){
array.push([key, hash(value)])
}).sort()
default: return object.toString()
}
},
/**
* Return last element of an array.
*
* @param {array} array
* @return {object}
* @api public
*/
last : function(array) {
return array[array.length - 1]
},
/**
* Convert object(s) to a print-friend string.
*
* @param {...} object
* @return {string}
* @api public
*/
puts : function(object) {
if (arguments.length > 1) {
return map(argumentsToArray(arguments), function(arg){
return puts(arg)
}).join(', ')
}
if (object === undefined) return ''
if (object === null) return 'null'
if (object === true) return 'true'
if (object === false) return 'false'
if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name
if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + ''
if (object.jquery) return escape(object.html())
if (object.nodeName) return escape(object.outerHTML)
switch (object.constructor) {
case String: return "'" + escape(object) + "'"
case Number: return object
case Function: return object.name || object
case Array :
return inject(object, '[', function(b, v){
return b + ', ' + puts(v)
}).replace('[,', '[') + ' ]'
case Object:
return inject(object, '{', function(b, k, v) {
return b + ', ' + puts(k) + ' : ' + puts(v)
}).replace('{,', '{') + ' }'
default:
return escape(object.toString())
}
},
/**
* Escape HTML.
*
* @param {string} html
* @return {string}
* @api public
*/
escape : function(html) {
return html.toString().
replace(/&/gmi, '&amp;').
replace(/"/gmi, '&quot;').
replace(/>/gmi, '&gt;').
replace(/</gmi, '&lt;')
},
/**
* Perform an assertion without reporting.
*
* This method is primarily used for internal
* matchers in order retain DRYness. May be invoked
* like below:
*
* does('foo', 'eql', 'foo')
* does([1,2], 'include', 1, 2)
*
* External hooks are not run for internal assertions
* performed by does().
*
* @param {mixed} actual
* @param {string} matcher
* @param {...} expected
* @return {mixed}
* @api private
*/
does : function(actual, matcher, expected) {
var assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, argumentsToArray(arguments, 2))
return assertion.run().result
},
/**
* Perform an assertion.
*
* expect(true).to('be', true)
* expect('foo').not_to('include', 'bar')
* expect([1, [2]]).to('include', 1, [2])
*
* @param {mixed} actual
* @return {hash}
* @api public
*/
expect : function(actual) {
assert = function(matcher, args, negate) {
var expected = argumentsToArray(args, 1)
matcher.negate = negate
assertion = new JSpec.Assertion(matcher, actual, expected, negate)
hook('beforeAssertion', assertion)
if (matcher.defer) assertion.run()
else JSpec.currentSpec.assertions.push(assertion.run().report()), hook('afterAssertion', assertion)
return assertion.result
}
to = function(matcher) {
return assert(matcher, arguments, false)
}
not_to = function(matcher) {
return assert(matcher, arguments, true)
}
return {
to : to,
should : to,
not_to: not_to,
should_not : not_to
}
},
/**
* Strim whitespace or chars.
*
* @param {string} string
* @param {string} chars
* @return {string}
* @api public
*/
strip : function(string, chars) {
return string.
replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
},
/**
* Call an iterator callback with arguments a, or b
* depending on the arity of the callback.
*
* @param {function} callback
* @param {mixed} a
* @param {mixed} b
* @return {mixed}
* @api private
*/
callIterator : function(callback, a, b) {
return callback.length == 1 ? callback(b) : callback(a, b)
},
/**
* Extend an object with another.
*
* @param {object} object
* @param {object} other
* @api public
*/
extend : function(object, other) {
each(other, function(property, value){
object[property] = value
})
},
/**
* Iterate an object, invoking the given callback.
*
* @param {hash, array, string} object
* @param {function} callback
* @return {JSpec}
* @api public
*/
each : function(object, callback) {
if (typeof object == 'string') object = object.split(' ')
for (key in object)
if (object.hasOwnProperty(key))
callIterator(callback, key, object[key])
},
/**
* Iterate with memo.
*
* @param {hash, array} object
* @param {object} memo
* @param {function} callback
* @return {object}
* @api public
*/
inject : function(object, memo, callback) {
each(object, function(key, value){
memo = (callback.length == 2 ?
callback(memo, value):
callback(memo, key, value)) ||
memo
})
return memo
},
/**
* Destub _object_'s _method_. When no _method_ is passed
* all stubbed methods are destubbed. When no arguments
* are passed every object found in JSpec.stubbed will be
* destubbed.
*
* @param {mixed} object
* @param {string} method
* @api public
*/
destub : function(object, method) {
if (method) {
if (object['__prototype__' + method])
delete object[method]
else
object[method] = object['__original__' + method]
delete object['__prototype__' + method]
delete object['__original____' + method]
}
else if (object) {
for (var key in object)
if (captures = key.match(/^(?:__prototype__|__original__)(.*)/))
destub(object, captures[1])
}
else
while (JSpec.stubbed.length)
destub(JSpec.stubbed.shift())
},
/**
* Stub _object_'s _method_.
*
* stub(foo, 'toString').and_return('bar')
*
* @param {mixed} object
* @param {string} method
* @return {hash}
* @api public
*/
stub : function(object, method) {
hook('stubbing', object, method)
JSpec.stubbed.push(object)
var type = object.hasOwnProperty(method) ? '__original__' : '__prototype__'
object[type + method] = object[method]
object[method] = function(){}
return {
and_return : function(value) {
if (typeof value == 'function') object[method] = value
else object[method] = function(){ return value }
}
}
},
/**
* Map callback return values.
*
* @param {hash, array} object
* @param {function} callback
* @return {array}
* @api public
*/
map : function(object, callback) {
return inject(object, [], function(memo, key, value){
memo.push(callIterator(callback, key, value))
})
},
/**
* Returns the first matching expression or null.
*
* @param {hash, array} object
* @param {function} callback
* @return {mixed}
* @api public
*/
any : function(object, callback) {
return inject(object, null, function(state, key, value){
if (state == undefined)
return callIterator(callback, key, value) ? value : state
})
},
/**
* Returns an array of values collected when the callback
* given evaluates to true.
*
* @param {hash, array} object
* @return {function} callback
* @return {array}
* @api public
*/
select : function(object, callback) {
return inject(object, [], function(selected, key, value){
if (callIterator(callback, key, value))
selected.push(value)
})
},
/**
* Define matchers.
*
* @param {hash} matchers
* @api public
*/
addMatchers : function(matchers) {
each(matchers, function(name, body){
JSpec.addMatcher(name, body)
})
},
/**
* Define a matcher.
*
* @param {string} name
* @param {hash, function, string} body
* @api public
*/
addMatcher : function(name, body) {
hook('addingMatcher', name, body)
if (name.indexOf(' ') != -1) {
var matchers = name.split(/\s+/)
var prefix = matchers.shift()
each(matchers, function(name) {
JSpec.addMatcher(prefix + '_' + name, body(name))
})
}
this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body))
this.matchers[name].name = name
},
/**
* Add a root suite to JSpec.
*
* @param {string} description
* @param {body} function
* @api public
*/
describe : function(description, body) {
var suite = new JSpec.Suite(description, body)
hook('addingSuite', suite)
this.allSuites.push(suite)
this.suites.push(suite)
},
/**
* Return the contents of a function body.
*
* @param {function} body
* @return {string}
* @api public
*/
contentsOf : function(body) {
return body.toString().match(/^[^\{]*{((.*\n*)*)}/m)[1]
},
/**
* Return param names for a function body.
*
* @param {function} body
* @return {array}
* @api public
*/
paramsFor : function(body) {
return body.toString().match(/\((.*?)\)/)[1].match(/[\w]+/g) || []
},
/**
* Evaluate a JSpec capture body.
*
* @param {function} body
* @param {string} errorMessage (optional)
* @return {Type}
* @api private
*/
evalBody : function(body, errorMessage) {
var dsl = this.DSL || this.DSLs.snake
var matchers = this.matchers
var context = this.context || this.defaultContext
var contents = this.contentsOf(body)
hook('evaluatingBody', dsl, matchers, context, contents)
try { eval('with (dsl){ with (context) { with (matchers) { ' + contents + ' }}}') }
catch(e) { error(errorMessage, e) }
},
/**
* Pre-process a string of JSpec.
*
* @param {string} input
* @return {string}
* @api private
*/
preprocess : function(input) {
input = hookImmutable('preprocessing', input)
return input.
replace(/([\w\.]+)\.(stub|destub)\((.*?)\)$/gm, '$2($1, $3)').
replace(/describe\s+(.*?)$/gm, 'describe($1, function(){').
replace(/^\s+it\s+(.*?)$/gm, ' it($1, function(){').
replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){').
replace(/^\s*end(?=\s|$)/gm, '});').
replace(/-\{/g, 'function(){').
replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }).
replace(/\.should([_\.]not)?[_\.](\w+)(?: |;|$)(.*)$/gm, '.should$1_$2($3)').
replace(/([\/\s]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)\s*;?$/gm, '$1 expect($2).$3($4, $5)').
replace(/, \)/gm, ')').
replace(/should\.not/gm, 'should_not')
},
/**
* Create a range string which can be evaluated to a native array.
*
* @param {int} start
* @param {int} end
* @return {string}
* @api public
*/
range : function(start, end) {
var current = parseInt(start), end = parseInt(end), values = [current]
if (end > current) while (++current <= end) values.push(current)
else while (--current >= end) values.push(current)
return '[' + values + ']'
},
/**
* Report on the results.
*
* @api public
*/
report : function() {
hook('reporting', JSpec.options)
new (JSpec.options.formatter || JSpec.formatters.DOM)(JSpec, JSpec.options)
},
/**
* Run the spec suites. Options are merged
* with JSpec options when present.
*
* @param {hash} options
* @return {JSpec}
* @api public
*/
run : function(options) {
if (any(hook('running'), haveStopped)) return this
if (options) extend(this.options, options)
if (option('profile')) console.group('Profile')
each(this.suites, function(suite) { JSpec.runSuite(suite) })
if (option('profile')) console.groupEnd()
return this
},
/**
* Run a suite.
*
* @param {Suite} suite
* @api public
*/
runSuite : function(suite) {
this.currentSuite = suite
this.evalBody(suite.body)
suite.ran = true
hook('beforeSuite', suite), suite.hook('before')
each(suite.specs, function(spec) {
hook('beforeSpec', spec)
suite.hook('before_each')
JSpec.runSpec(spec)
hook('afterSpec', spec)
suite.hook('after_each')
})
if (suite.hasSuites()) {
each(suite.suites, function(suite) {
JSpec.runSuite(suite)
})
}
hook('afterSuite', suite), suite.hook('after')
this.stats.suitesFinished++
},
/**
* Report a failure for the current spec.
*
* @param {string} message
* @api public
*/
fail : function(message) {
JSpec.currentSpec.fail(message)
},
/**
* Report a passing assertion for the current spec.
*
* @param {string} message
* @api public
*/
pass : function(message) {
JSpec.currentSpec.pass(message)
},
/**
* Run a spec.
*
* @param {Spec} spec
* @api public
*/
runSpec : function(spec) {
this.currentSpec = spec
if (option('profile')) console.time(spec.description)
try { this.evalBody(spec.body) }
catch (e) { fail(e) }
if (option('profile')) console.timeEnd(spec.description)
spec.runDeferredAssertions()
destub()
this.stats.specsFinished++
this.stats.assertions += spec.assertions.length
},
/**
* Require a dependency, with optional message.
*
* @param {string} dependency
* @param {string} message (optional)
* @return {JSpec}
* @api public
*/
requires : function(dependency, message) {
hook('requiring', dependency, message)
try { eval(dependency) }
catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message }
return this
},
/**
* Query against the current query strings keys
* or the queryString specified.
*
* @param {string} key
* @param {string} queryString
* @return {string, null}
* @api private
*/
query : function(key, queryString) {
var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1)
return inject(queryString.split('&'), null, function(value, pair){
parts = pair.split('=')
return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value
})
},
/**
* Throw a JSpec related error.
*
* @param {string} message
* @param {Exception} e
* @api public
*/
error : function(message, e) {
throw (message ? message : '') + e.toString() +
(e.line ? ' near line ' + e.line : '')
},
/**
* Ad-hoc POST request for JSpec server usage.
*
* @param {string} url
* @param {string} data
* @api private
*/
post : function(url, data) {
if (any(hook('posting', url, data), haveStopped)) return
var request = this.xhr()
request.open('POST', url, false)
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
request.send(data)
},
/**
* Report to server with statistics.
*
* @param {string} url
* @api private
*/
reportToServer : function(url) {
if (any(hook('reportingToServer', url), haveStopped)) return
JSpec.post(url || 'http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures)
if ('close' in main) main.close()
},
/**
* Instantiate an XMLHttpRequest.
*
* @return {XMLHttpRequest, ActiveXObject}
* @api private
*/
xhr : function() {
return new (JSpec.request || ActiveXObject("Microsoft.XMLHTTP"))
},
/**
* Check for HTTP request support.
*
* @return {bool}
* @api private
*/
hasXhr : function() {
return JSpec.request || 'ActiveXObject' in main
},
/**
* Try loading _file_ returning the contents
* string or null. Chain to locate / read a file.
*
* @param {string} file
* @return {string}
* @api public
*/
tryLoading : function(file) {
try { return JSpec.load(file) }
catch (e) {}
},
/**
* Load a _file_'s contents.
*
* @param {string} file
* @param {function} callback
* @return {string}
* @api public
*/
load : function(file, callback) {
if (any(hook('loading', file), haveStopped)) return
if ('readFile' in main)
return callback ? readFile(file, callback) : readFile(file)
else if (this.hasXhr()) {
var request = this.xhr()
request.open('GET', file, false)
request.send(null)
if (request.readyState == 4) return request.responseText
}
else
error("failed to load `" + file + "'")
},
/**
* Load, pre-process, and evaluate a file.
*
* @param {string} file
* @param {JSpec}
* @api public
*/
exec : function(file) {
if (any(hook('executing', file), haveStopped)) return this
if ('node' in main)
this.load(file, function(contents){
eval('with (JSpec){ ' + JSpec.preprocess(contents) + ' }')
})
else
eval('with (JSpec){' + this.preprocess(this.load(file)) + '}')
return this
}
}
// --- Utility functions
var main = this
var find = JSpec.any
var utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \
error escape extend puts hash query strip color does addMatchers callIterator argumentsToArray'.split(/\s+/)
while (utils.length) util = utils.shift(), eval('var ' + util + ' = JSpec.' + util)
if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
// --- Matchers
addMatchers({
equal : "===",
be : "alias equal",
be_greater_than : ">",
be_less_than : "<",
be_at_least : ">=",
be_at_most : "<=",
be_a : "actual.constructor == expected",
be_an : "alias be_a",
be_an_instance_of : "actual instanceof expected",
be_null : "actual == null",
be_true : "actual == true",
be_false : "actual == false",
be_undefined : "typeof actual == 'undefined'",
be_type : "typeof actual == expected",
match : "typeof actual == 'string' ? actual.match(expected) : false",
respond_to : "typeof actual[expected] == 'function'",
have_length : "actual.length == expected",
be_within : "actual >= expected[0] && actual <= last(expected)",
have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)",
eql : function(actual, expected) {
return actual.constructor == Array ||
actual instanceof Object ?
hash(actual) == hash(expected):
actual == expected
},
receive : { defer : true, match : function(actual, method, times) {
proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate)
JSpec.currentSpec.assertions.push(proxy)
return proxy
}},
be_empty : function(actual) {
if (actual.constructor == Object && actual.length == undefined)
for (var key in actual)
return false;
return !actual.length
},
include : function(actual) {
for (state = true, i = 1; i < arguments.length; i++) {
arg = arguments[i]
switch (actual.constructor) {
case String:
case Number:
case RegExp:
case Function:
state = actual.toString().match(arg.toString())
break
case Object:
state = arg in actual
break
case Array:
state = any(actual, function(value){ return hash(value) == hash(arg) })
break
}
if (!state) return false
}
return true
},
throw_error : { match : function(actual, expected, message) {
try { actual() }
catch (e) {
this.e = e
var assert = function(arg) {
switch (arg.constructor) {
case RegExp : return arg.test(e)
case String : return arg == (e.message || e.toString())
case Function : return (e.name || 'Error') == arg.name
}
}
return message ? assert(expected) && assert(message) :
expected ? assert(expected) :
true
}
}, message : function(actual, expected, negate) {
// TODO: refactor when actual is not in expected [0]
var message_for = function(i) {
if (expected[i] == undefined) return 'exception'
switch (expected[i].constructor) {
case RegExp : return 'exception matching ' + puts(expected[i])
case String : return 'exception of ' + puts(expected[i])
case Function : return expected[i].name || 'Error'
}
}
exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '')
return 'expected ' + exception + (negate ? ' not ' : '' ) +
' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was')
}},
have : function(actual, length, property) {
return actual[property].length == length
},
have_at_least : function(actual, length, property) {
return actual[property].length >= length
},
have_at_most :function(actual, length, property) {
return actual[property].length <= length
},
have_within : function(actual, range, property) {
length = actual[property].length
return length >= range.shift() && length <= range.pop()
},
have_prop : function(actual, property, value) {
return actual[property] == null ||
actual[property] instanceof Function ? false:
value == null ? true:
does(actual[property], 'eql', value)
},
have_property : function(actual, property, value) {
return actual[property] == null ||
actual[property] instanceof Function ? false:
value == null ? true:
value === actual[property]
}
})
if ('exports' in main) exports.JSpec = JSpec
})()