summaryrefslogtreecommitdiff
path: root/share/www/script/jspec/jspec.js
diff options
context:
space:
mode:
Diffstat (limited to 'share/www/script/jspec/jspec.js')
-rw-r--r--share/www/script/jspec/jspec.js1756
1 files changed, 1756 insertions, 0 deletions
diff --git a/share/www/script/jspec/jspec.js b/share/www/script/jspec/jspec.js
new file mode 100644
index 00000000..d6daf5ef
--- /dev/null
+++ b/share/www/script/jspec/jspec.js
@@ -0,0 +1,1756 @@
+
+// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
+
+;(function(){
+
+ JSpec = {
+ version : '3.3.2',
+ 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',
+ report = document.getElementById(id),
+ failuresOnly = option('failuresOnly'),
+ 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) {
+ var 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)
+ }
+ },
+
+ 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){
+ var newSpec = new Object();
+ extend(newSpec, spec);
+ newSpec.assertions = [];
+ toSuite.specs.push(newSpec);
+ })
+ },
+
+ /**
+ * 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, '&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, 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
+ }
+ }
+
+ // --- Node.js support
+
+ if (typeof GLOBAL === 'object' && typeof exports === 'object')
+ quit = process.exit,
+ print = require('sys').puts,
+ readFile = require('fs').readFileSync
+
+ // --- Utility functions
+
+ var main = this,
+ find = JSpec.any,
+ 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]
+ }
+ })
+
+})()