// JSpec - Core - Copyright TJ Holowaychuk (MIT Licensed) (function(){ JSpec = { version : '2.2.0', suites : [], allSuites : [], matchers : {}, 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 } }, /** * Sets the current spec's wait duration to _n_. * * wait(3000) * wait(1, 'second') * wait(3, 'seconds') * * @param {number} n * @param {string} unit * @api public */ wait : function(n, unit) { JSpec.currentSpec.wait = { 'second' : n * 1000, 'seconds' : n * 1000 }[unit] || n } }, // --- 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 = '
\ Passes: ' + results.stats.passes + ' \ Failures: ' + results.stats.failures + ' \
' bodyContents = function(body) { return JSpec. escape(JSpec.contentsOf(body)). replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }). replace("\n", '
') } renderSuite = function(suite) { var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran if (displaySuite && suite.hasSpecs()) { markup += '' each(suite.specs, function(i, spec){ markup += '' if (spec.requiresImplementation()) markup += '' else if (spec.passed() && !failuresOnly) markup += '' else if(!spec.passed()) markup += '' markup += '' }) markup += '' } } renderSuites = function(suites) { each(suites, function(suite){ renderSuite(suite) if (suite.hasSuites()) renderSuites(suite.suites) }) } renderSuites(results.suites) markup += '
' + suite.description + '
' + spec.description + '' + spec.description+ '' + spec.assertionsGraph() + '' + spec.description + ' ' + spec.failure().message + '' + '' + spec.assertionsGraph() + '
' + bodyContents(spec.body) + '
' 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) { 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 // TODO: negation extend(this, { calls : [], message : '', defer : true, passed : false, 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) }) }, // 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){ return arg.an_instance_of ? call.args[i].constructor != arg.an_instance_of: hash(arg) != hash(call.args[i]) }) }) }, // 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 + '()' function times(n) { return n > 2 ? n + ' times' : { 1 : 'once', 2 : 'twice' }[n] } if (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.expectedResult && this.anyResultsFail()) this.message = methodString + ' to return ' + puts(this.expectedResult) + ' but got ' + puts(this.failingResult()) if (this.expectedArgs && this.anyArgsFail()) this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) + ' but was called with ' + puts.apply(this, this.failingArgs()) 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 : [], // Run deferred assertions runDeferredAssertions : function() { each(this.assertions, function(assertion){ if (assertion.defer) assertion.run().report() }) }, // 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 '' }).join('') } }) }, // --- 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 /** * 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){ 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 == 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 Object: return serialize('o') case RegExp: return 'r:' + object.toString() case Number: return 'n:' + object.toString() case String: return 's:' + object.toString() 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, ...} 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, '&'). replace(/"/gmi, '"'). replace(/>/gmi, '>'). replace(/ current) while (++current <= end) values.push(current) else while (--current >= end) values.push(current) return '[' + values + ']' }, /** * Call _callback_ when all specs have finished. * * @param {function} callback * @api public */ whenFinished : function(callback) { if (this.stats.specsFinished >= this.stats.specs) callback() else setTimeout(function(){ JSpec.whenFinished(callback) }, 50) }, /** * Report on the results. * * @api public */ report : function() { this.whenFinished(function() { JSpec.options.formatter ? new JSpec.options.formatter(JSpec, JSpec.options): new 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 (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 }, /** * When the current spec's wait duration has passed * the _callback_ will be called. * * @param {function} callback * @api public */ whenCurrentSpecIsFinished : function(callback) { if (this.currentSpec && this.currentSpec.wait) setTimeout(callback, this.currentSpec.wait) else callback() }, /** * Run a suite. * * @param {Suite} suite * @api public */ runSuite : function(suite) { this.currentSuite = suite this.evalBody(suite.body) suite.ran = true suite.hook('before') each(suite.specs, function(spec) { JSpec.whenCurrentSpecIsFinished(function(){ suite.hook('before_each') JSpec.runSpec(spec) suite.hook('after_each') }) }) if (suite.hasSuites()) { each(suite.suites, function(suite) { JSpec.runSuite(suite) }) } suite.hook('after') this.stats.suitesFinished++ }, /** * Report a failure for the current spec. * * @param {string} message * @api public */ fail : function(message) { JSpec.currentSpec.assertions.push({ passed : false, message : message }) JSpec.stats.failures++ }, /** * 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) } spec.runDeferredAssertions() if (option('profile')) console.timeEnd(spec.description) this.stats.specsFinished++ this.stats.assertions += spec.assertions.length }, /** * Require a dependency, with optional message. * * @param {string} dependency * @param {string} message (optional) * @api public */ requires : function(dependency, message) { try { eval(dependency) } catch (e) { error('JSpec depends on ' + dependency + ' ' + message) } }, /** * 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) { var request = this.xhr() request.open('POST', url, false) request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') request.send(data) }, /** * Report back to server with statistics. * * @api private */ reportToServer : function() { this.whenFinished(function(){ JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures) if ('close' in main) main.close() }) }, /** * Instantiate an XMLHttpRequest. * * @return {ActiveXObject, XMLHttpRequest} * @api private */ xhr : function() { return 'ActiveXObject' in main ? new ActiveXObject("Microsoft.XMLHTTP"): new XMLHttpRequest() }, /** * Check for HTTP request support. * * @return {bool} * @api private */ hasXhr : function() { return 'XMLHttpRequest' in main || 'ActiveXObject' in main }, /** * Load a files contents. * * @param {string} file * @return {string} * @api public */ load : function(file) { if (this.hasXhr()) { var request = this.xhr() request.open('GET', file, false) request.send(null) if (request.readyState == 4) return request.responseText } else if ('readFile' in main) return readFile(file) else error('cannot load ' + file) }, /** * Load, pre-process, and evaluate a file. * * @param {string} file * @param {JSpec} * @api public */ exec : function(file) { eval('with (JSpec){' + this.preprocess(this.load(file)) + '}') return this } } // --- Utility functions var main = this var map = JSpec.map var any = JSpec.any var find = JSpec.any var last = JSpec.last var fail = JSpec.fail var range = JSpec.range var each = JSpec.each var option = JSpec.option var inject = JSpec.inject var select = JSpec.select var error = JSpec.error var escape = JSpec.escape var extend = JSpec.extend var puts = JSpec.puts var hash = JSpec.hash var query = JSpec.query var strip = JSpec.strip var color = JSpec.color var does = JSpec.does var addMatchers = JSpec.addMatchers var callIterator = JSpec.callIterator var argumentsToArray = JSpec.argumentsToArray 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_empty : "actual.length == 0", be_true : "actual == true", be_false : "actual == false", 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) JSpec.currentSpec.assertions.push(proxy) return proxy }}, 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) { console.log(e.constructor); switch (arg.constructor) { case RegExp : return arg.test(e) case Function : return e.constructor == arg case String : return arg == (e.message || e.toString()) } } 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 Function : return expected[i].name case String : return 'exception of ' + puts(expected[i]) } } if (expected[1] == Error && expected[2] == 'oh no!') { console.log(expected); } 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] } }) })()