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