1774 lines
51 KiB
JavaScript
1774 lines
51 KiB
JavaScript
|
|
// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
|
|
|
;(function(){
|
|
|
|
JSpec = {
|
|
version : '3.1.3',
|
|
assert : true,
|
|
cache : {},
|
|
suites : [],
|
|
modules : [],
|
|
allSuites : [],
|
|
matchers : {},
|
|
stubbed : [],
|
|
options : {},
|
|
request : 'XMLHttpRequest' in this ? XMLHttpRequest : null,
|
|
stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 },
|
|
|
|
/**
|
|
* 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_.
|
|
*
|
|
* Fixtures are resolved as:
|
|
*
|
|
* - <path>
|
|
* - <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(JSpec.options.fixturePath + '/' + path) ||
|
|
JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html')
|
|
}
|
|
},
|
|
|
|
// --- Objects
|
|
|
|
reporters : {
|
|
|
|
/**
|
|
* Report to server.
|
|
*
|
|
* Options:
|
|
* - uri specific uri to report to.
|
|
* - verbose weither or not to output messages
|
|
* - failuresOnly output failure messages only
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Server : function(results, options) {
|
|
var uri = options.uri || 'http://' + window.location.host + '/results'
|
|
JSpec.post(uri, {
|
|
stats: JSpec.stats,
|
|
options: options,
|
|
results: map(results.allSuites, function(suite) {
|
|
if (suite.hasSpecs())
|
|
return {
|
|
description: suite.description,
|
|
specs: map(suite.specs, function(spec) {
|
|
return {
|
|
description: spec.description,
|
|
message: !spec.passed() ? spec.failure().message : null,
|
|
status: spec.requiresImplementation() ? 'pending' :
|
|
spec.passed() ? 'pass' :
|
|
'fail',
|
|
assertions: map(spec.assertions, function(assertion){
|
|
return {
|
|
passed: assertion.passed
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
if ('close' in main) main.close()
|
|
},
|
|
|
|
/**
|
|
* Default reporter, 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'
|
|
|
|
function bodyContents(body) {
|
|
return JSpec.
|
|
escape(JSpec.contentsOf(body)).
|
|
replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }).
|
|
replace(/\r\n|\r|\n/gm, '<br/>')
|
|
}
|
|
|
|
report.innerHTML = '<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> \
|
|
<span class="passes">Duration: <em>' + results.duration + '</em> ms</span> \
|
|
</div><table class="suites">' + map(results.allSuites, function(suite) {
|
|
var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
|
|
if (displaySuite && suite.hasSpecs())
|
|
return '<tr class="description"><td colspan="2">' + escape(suite.description) + '</td></tr>' +
|
|
map(suite.specs, function(i, spec) {
|
|
return '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' +
|
|
(spec.requiresImplementation() ?
|
|
'<td class="requires-implementation" colspan="2">' + escape(spec.description) + '</td>' :
|
|
(spec.passed() && !failuresOnly) ?
|
|
'<td class="pass">' + escape(spec.description)+ '</td><td>' + spec.assertionsGraph() + '</td>' :
|
|
!spec.passed() ?
|
|
'<td class="fail">' + escape(spec.description) +
|
|
map(spec.failures(), function(a){ return '<em>' + escape(a.message) + '</em>' }).join('') +
|
|
'</td><td>' + spec.assertionsGraph() + '</td>' :
|
|
'') +
|
|
'<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>'
|
|
}).join('') + '</tr>'
|
|
}).join('') + '</table></div>'
|
|
},
|
|
|
|
/**
|
|
* Terminal reporter.
|
|
*
|
|
* @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') +
|
|
color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n")
|
|
|
|
function indent(string) {
|
|
return string.replace(/^(.)/gm, ' $1')
|
|
}
|
|
|
|
each(results.allSuites, function(suite) {
|
|
var 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(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n")
|
|
})
|
|
print("")
|
|
}
|
|
})
|
|
|
|
quit(results.stats.failures)
|
|
},
|
|
|
|
/**
|
|
* Console reporter.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Console : function(results, options) {
|
|
console.log('')
|
|
console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
|
|
each(results.allSuites, 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()
|
|
}
|
|
})
|
|
}
|
|
},
|
|
|
|
Assertion : function(matcher, actual, expected, negate) {
|
|
extend(this, {
|
|
message: '',
|
|
passed: false,
|
|
actual: actual,
|
|
negate: negate,
|
|
matcher: matcher,
|
|
expected: expected,
|
|
|
|
// Report assertion results
|
|
|
|
report : function() {
|
|
if (JSpec.assert)
|
|
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 = toArray(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 = toArray(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:
|
|
!equal(self.expectedResult, 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:
|
|
equal(self.expectedResult, 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:
|
|
!equal(arg, 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:
|
|
equal(arg, 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() {
|
|
if (JSpec.assert)
|
|
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 })
|
|
if (JSpec.assert) ++JSpec.stats.passes
|
|
},
|
|
|
|
// Add failing assertion
|
|
|
|
fail : function(message) {
|
|
this.assertions.push({ passed: false, message: message })
|
|
if (JSpec.assert) ++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)
|
|
},
|
|
|
|
JSON : {
|
|
|
|
/**
|
|
* Generic sequences.
|
|
*/
|
|
|
|
meta : {
|
|
'\b' : '\\b',
|
|
'\t' : '\\t',
|
|
'\n' : '\\n',
|
|
'\f' : '\\f',
|
|
'\r' : '\\r',
|
|
'"' : '\\"',
|
|
'\\' : '\\\\'
|
|
},
|
|
|
|
/**
|
|
* Escapable sequences.
|
|
*/
|
|
|
|
escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
|
|
|
/**
|
|
* JSON encode _object_.
|
|
*
|
|
* @param {mixed} object
|
|
* @return {string}
|
|
* @api private
|
|
*/
|
|
|
|
encode : function(object) {
|
|
var self = this
|
|
if (object == undefined || object == null) return 'null'
|
|
if (object === true) return 'true'
|
|
if (object === false) return 'false'
|
|
switch (typeof object) {
|
|
case 'number': return object
|
|
case 'string': return this.escapable.test(object) ?
|
|
'"' + object.replace(this.escapable, function (a) {
|
|
return typeof self.meta[a] === 'string' ? self.meta[a] :
|
|
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
|
|
}) + '"' :
|
|
'"' + object + '"'
|
|
case 'object':
|
|
if (object.constructor == Array)
|
|
return '[' + map(object, function(val){
|
|
return self.encode(val)
|
|
}).join(', ') + ']'
|
|
else if (object)
|
|
return '{' + map(object, function(key, val){
|
|
return self.encode(key) + ':' + self.encode(val)
|
|
}).join(', ') + '}'
|
|
}
|
|
return 'null'
|
|
}
|
|
},
|
|
|
|
// --- 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 {hash, 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 ('reporters' in module) extend(this.reporters, module.reporters)
|
|
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 = toArray(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) {
|
|
hook('evaluatingHookBody', module, name)
|
|
try { return module[name].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
|
|
*/
|
|
|
|
toArray : 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, ' ') +
|
|
' ' + (expected.length > 1 ?
|
|
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
|
|
},
|
|
|
|
/**
|
|
* Check if object _a_, is equal to object _b_.
|
|
*
|
|
* @param {object} a
|
|
* @param {object} b
|
|
* @return {bool}
|
|
* @api private
|
|
*/
|
|
|
|
equal: function(a, b) {
|
|
if (typeof a != typeof b) return
|
|
if (a === b) return true
|
|
if (a instanceof RegExp)
|
|
return a.toString() === b.toString()
|
|
if (a instanceof Date)
|
|
return Number(a) === Number(b)
|
|
if (typeof a != 'object') return
|
|
if (a.length !== undefined)
|
|
if (a.length !== b.length) return
|
|
else
|
|
for (var i = 0, len = a.length; i < len; ++i)
|
|
if (!equal(a[i], b[i]))
|
|
return
|
|
for (var key in a)
|
|
if (!equal(a[key], b[key]))
|
|
return
|
|
return true
|
|
},
|
|
|
|
/**
|
|
* 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(toArray(arguments), function(arg){
|
|
return puts(arg)
|
|
}).join(', ')
|
|
if (object === undefined) return 'undefined'
|
|
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 object.get(0).outerHTML
|
|
if (object.nodeName) return object.outerHTML
|
|
switch (object.constructor) {
|
|
case Function: return object.name || object
|
|
case String:
|
|
return '"' + object
|
|
.replace(/"/g, '\\"')
|
|
.replace(/\n/g, '\\n')
|
|
.replace(/\t/g, '\\t')
|
|
+ '"'
|
|
case Array:
|
|
return inject(object, '[', function(b, v){
|
|
return b + ', ' + puts(v)
|
|
}).replace('[,', '[') + ' ]'
|
|
case Object:
|
|
object.__hit__ = true
|
|
return inject(object, '{', function(b, k, v) {
|
|
if (k == '__hit__') return b
|
|
return b + ', ' + k + ': ' + (v && v.__hit__ ? '<circular reference>' : puts(v))
|
|
}).replace('{,', '{') + ' }'
|
|
default:
|
|
return object.toString()
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Escape HTML.
|
|
*
|
|
* @param {string} html
|
|
* @return {string}
|
|
* @api public
|
|
*/
|
|
|
|
escape : function(html) {
|
|
return html.toString()
|
|
.replace(/&/gmi, '&')
|
|
.replace(/"/gmi, '"')
|
|
.replace(/>/gmi, '>')
|
|
.replace(/</gmi, '<')
|
|
},
|
|
|
|
/**
|
|
* 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, toArray(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) {
|
|
function assert(matcher, args, negate) {
|
|
var expected = toArray(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
|
|
}
|
|
|
|
function to(matcher) {
|
|
return assert(matcher, arguments, false)
|
|
}
|
|
|
|
function not_to(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} object
|
|
* @param {function} callback
|
|
* @return {JSpec}
|
|
* @api public
|
|
*/
|
|
|
|
each : function(object, callback) {
|
|
if (object.constructor == Array)
|
|
for (var i = 0, len = object.length; i < len; ++i)
|
|
callIterator(callback, i, object[i])
|
|
else
|
|
for (var 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]
|
|
},
|
|
|
|
/**
|
|
* 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 { with (dsl){ with (context) { with (matchers) { eval(contents) }}} }
|
|
catch(e) { error(errorMessage, e) }
|
|
},
|
|
|
|
/**
|
|
* Pre-process a string of JSpec.
|
|
*
|
|
* @param {string} input
|
|
* @return {string}
|
|
* @api private
|
|
*/
|
|
|
|
preprocess : function(input) {
|
|
if (typeof input != 'string') return
|
|
input = hookImmutable('preprocessing', input)
|
|
return input.
|
|
replace(/\t/g, ' ').
|
|
replace(/\r\n|\n|\r/g, '\n').
|
|
split('__END__')[0].
|
|
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(/, \)/g, ')').
|
|
replace(/should\.not/g, '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() {
|
|
this.duration = Number(new Date) - this.start
|
|
hook('reporting', JSpec.options)
|
|
new (JSpec.options.reporter || JSpec.reporters.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)
|
|
this.start = Number(new Date)
|
|
each(this.suites, function(suite) { JSpec.runSuite(suite) })
|
|
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
|
|
try { this.evalBody(spec.body) }
|
|
catch (e) { fail(e) }
|
|
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} uri
|
|
* @param {string} data
|
|
* @api private
|
|
*/
|
|
|
|
post : function(uri, data) {
|
|
if (any(hook('posting', uri, data), haveStopped)) return
|
|
var request = this.xhr()
|
|
request.open('POST', uri, false)
|
|
request.setRequestHeader('Content-Type', 'application/json')
|
|
request.send(JSpec.JSON.encode(data))
|
|
},
|
|
|
|
/**
|
|
* Instantiate an XMLHttpRequest.
|
|
*
|
|
* Here we utilize IE's lame ActiveXObjects first which
|
|
* allow IE access serve files via the file: protocol, otherwise
|
|
* we then default to XMLHttpRequest.
|
|
*
|
|
* @return {XMLHttpRequest, ActiveXObject}
|
|
* @api private
|
|
*/
|
|
|
|
xhr : function() {
|
|
return this.ieXhr() || new JSpec.request
|
|
},
|
|
|
|
/**
|
|
* Return Microsoft piece of crap ActiveXObject.
|
|
*
|
|
* @return {ActiveXObject}
|
|
* @api public
|
|
*/
|
|
|
|
ieXhr : function() {
|
|
function object(str) {
|
|
try { return new ActiveXObject(str) } catch(e) {}
|
|
}
|
|
return object('Msxml2.XMLHTTP.6.0') ||
|
|
object('Msxml2.XMLHTTP.3.0') ||
|
|
object('Msxml2.XMLHTTP') ||
|
|
object('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 readFile(file)
|
|
else if (this.hasXhr()) {
|
|
var request = this.xhr()
|
|
request.open('GET', file, false)
|
|
request.send(null)
|
|
if (request.readyState == 4 &&
|
|
(request.status == 0 ||
|
|
request.status.toString().charAt(0) == 2))
|
|
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
|
|
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 query strip color does addMatchers callIterator toArray equal'.split(/\s+/)
|
|
while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift())
|
|
if (!main.setTimeout) main.setTimeout = function(callback){ callback() }
|
|
|
|
// --- Matchers
|
|
|
|
addMatchers({
|
|
equal : "===",
|
|
eql : "equal(actual, expected)",
|
|
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)",
|
|
|
|
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().indexOf(arg) !== -1
|
|
break
|
|
|
|
case Object:
|
|
state = arg in actual
|
|
break
|
|
|
|
case Array:
|
|
state = any(actual, function(value){ return equal(value, 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.message || e.toString())
|
|
case String : return arg == (e.message || e.toString())
|
|
case Function : return e instanceof arg || e.name == 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]
|
|
}
|
|
})
|
|
|
|
})()
|