lib/jspec.js in visionmedia-jspec-1.1.3 vs lib/jspec.js in visionmedia-jspec-1.1.4

- old
+ new

@@ -3,12 +3,12 @@ (function(){ JSpec = { - version : '1.1.3', - main : this, + version : '1.1.4', + file : '', suites : [], matchers : {}, stats : { specs : 0, assertions : 0, failures : 0, passes : 0 }, options : { profile : false }, @@ -27,10 +27,11 @@ * 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 : { sandbox : function(name) { sandbox = document.createElement('div') @@ -39,110 +40,11 @@ return sandbox } }, // --- Objects - - /** - * Matcher. - * - * There are many ways to define a matcher within JSpec. The first being - * a string that is less than 4 characters long, which is considered a simple - * binary operation between two expressions. For example the matcher '==' simply - * evaluates to 'actual == expected'. - * - * The second way to create a matcher is with a larger string, which is evaluated, - * and then returned such as 'actual.match(expected)'. - * - * You may alias simply by starting a string with 'alias', such as 'be' : 'alias eql'. - * - * Finally an object may be used, and must contain a 'match' method, which is passed - * both the expected, and actual values. Optionally a 'message' method may be used to - * specify a custom message. Example: - * - * match : function(actual, expected) { - * return typeof actual == expected - * } - * - * @param {string} name - * @param {hash, string} matcher - * @param {object} actual - * @param {array} expected - * @param {bool} negate - * @return {Matcher} - * @api private - */ - - Matcher : function (name, matcher, actual, expected, negate) { - self = this - this.name = name - this.message = '' - this.passed = false - - // Define matchers from strings - - if (typeof matcher == 'string') { - if (matcher.match(/^alias (\w+)/)) matcher = JSpec.matchers[matcher.match(/^alias (\w+)/)[1]] - if (matcher.length < 4) body = 'actual ' + matcher + ' expected' - else body = matcher - matcher = { match : function(actual, expected) { return eval(body) } } - } - - // Generate matcher message - - function generateMessage() { - // TODO: clone expected instead of unshifting in this.match() - expectedMessage = print.apply(this, expected.slice(1)) - return 'expected ' + print(actual) + ' to ' + (negate ? ' not ' : '') + name.replace(/_/g, ' ') + ' ' + expectedMessage - } - - // Set message to matcher callback invocation or auto-generated message - - function setMessage() { - self.message = typeof matcher.message == 'function' ? - matcher.message(actual, expected, negate): - generateMessage() - } - - // Pass the matcher - - function pass() { - setMessage() - JSpec.stats.passes += 1 - self.passed = true - } - - // Fail the matcher - - function fail() { - setMessage() - JSpec.stats.failures += 1 - } - - // Return result of match - - this.match = function() { - expected.unshift(actual == null ? null : actual.valueOf()) - return matcher.match.apply(JSpec, expected) - } - - // Boolean match result - - this.passes = function() { - this.result = this.match() - return negate? !this.result : this.result - } - - // Performs match, and passes / fails the matcher - - this.exec = function() { - this.passes() ? pass() : fail() - return this - } - }, - - + formatters : { /** * Default formatter, outputting to the DOM. * @@ -154,27 +56,27 @@ */ DOM : function(results, options) { id = option('reportToId') || 'jspec' report = document.getElementById(id) + failuresOnly = option('failuresOnly') classes = results.stats.failures ? 'has-failures' : '' if (!report) error('requires the element #' + id + ' to output its reports') markup = '<div id="jspec-report" class="' + classes + '"><div class="heading"> \ <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \ <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \ </div><table class="suites">' - function renderSuite(suite) { - failuresOnly = option('failuresOnly') + renderSuite = function(suite) { displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran if (displaySuite && suite.hasSpecs()) { markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>' each(suite.specs, function(i, spec){ markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">' - if (spec.requiresImplementation() && !failuresOnly) + if (spec.requiresImplementation()) markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>' else if (spec.passed() && !failuresOnly) markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>' else if(!spec.passed()) markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>' @@ -182,11 +84,11 @@ }) markup += '</tr>' } } - function renderSuites(suites) { + renderSuites = function(suites) { each(suites, function(suite){ renderSuite(suite) if (suite.hasSuites()) renderSuites(suite.suites) }) } @@ -195,22 +97,67 @@ markup += '</table></div>' report.innerHTML = markup }, + + /** + * Terminal formatter. + * + * @api public + */ + + Terminal : function(results, options) { + failuresOnly = option('failuresOnly') + puts(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()) { + puts(color(' ' + suite.description, 'bold')) + results.each(suite.specs, function(spec){ + assertionsGraph = inject(spec.assertions, '', function(graph, assertion){ + return graph + color('.', assertion.passed ? 'green' : 'red') + }) + if (spec.requiresImplementation()) + puts(color(' ' + spec.description, 'blue') + assertionsGraph) + else if (spec.passed() && !failuresOnly) + puts(color(' ' + spec.description, 'green') + assertionsGraph) + else + puts(color(' ' + spec.description, 'red') + assertionsGraph + + "\n" + indent(spec.failure().message) + "\n") + }) + puts('') + } + } + + 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) - function renderSuite(suite) { + renderSuite = function(suite) { if (suite.ran) { console.group(suite.description) results.each(suite.specs, function(spec){ assertionCount = spec.assertions.length + ':' if (spec.requiresImplementation()) @@ -222,86 +169,115 @@ }) console.groupEnd() } } - function renderSuites(suites) { + 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, + record : function(result) { + result ? JSpec.stats.passes++ : JSpec.stats.failures++ + }, + + exec : function() { + // TODO: remove unshifting of expected + expected.unshift(actual == null ? null : actual.valueOf()) + result = matcher.match.apply(JSpec, expected) + this.passed = negate ? !result : result + this.record(this.passed) + if (!this.passed) this.message = matcher.message(actual, expected, negate, matcher.name) + return this + } + }) + }, + /** * Specification Suite block object. * * @param {string} description * @param {function} body * @api private */ Suite : function(description, body) { - this.body = body, this.suites = [], this.specs = [] - this.description = description, this.ran = false - this.hooks = { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] } + extend(this, { + body: body, + description: description, + suites: [], + specs: [], + ran: false, + hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }, + + // Add a spec to the suite - // Add a spec to the suite + it : function(description, body) { + spec = new JSpec.Spec(description, body) + this.specs.push(spec) + spec.suite = this + }, - this.addSpec = function(description, body) { - spec = new JSpec.Spec(description, body) - this.specs.push(spec) - spec.suite = this - } - - // Add a hook to the suite - - this.addHook = function(hook, body) { - this.hooks[hook].push(body) - } - - // Add a nested suite - - this.addSuite = function(description, body) { - suite = new JSpec.Suite(description, body) - suite.description = this.description + ' ' + suite.description - this.suites.push(suite) - suite.suite = this - } + // Add a hook to the suite - // Invoke a hook in context to this suite + addHook : function(hook, body) { + this.hooks[hook].push(body) + }, - this.hook = function(hook) { - each(this.hooks[hook], function(body) { - JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ") - }) - } - - // Check if nested suites are present - - this.hasSuites = function() { - return this.suites.length - } - - // Check if this suite has specs - - this.hasSpecs = function() { - return this.specs.length - } - - // Check if the entire suite passed + // Add a nested suite - this.passed = function() { - var passed = true - each(this.specs, function(spec){ - if (!spec.passed()) passed = false - }) - return passed - } + describe : function(description, body) { + suite = new JSpec.Suite(description, body) + suite.description = this.description + ' ' + suite.description + this.suites.push(suite) + suite.suite = this + }, + + // Invoke a hook in context to this suite + + hook : function(hook) { + each(this.hooks[hook], function(body) { + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.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() { + var passed = true + each(this.specs, function(spec){ + if (!spec.passed()) passed = false + }) + return passed + } + }) }, /** * Specification block object. * @@ -309,65 +285,153 @@ * @param {function} body * @api private */ Spec : function(description, body) { - this.body = body, this.description = description, this.assertions = [] + extend(this, { + body : body, + description : description, + assertions : [], + + // Find first failing assertion - // Find first failing assertion + failure : function() { + return inject(this.assertions, null, function(failure, assertion){ + return !assertion.passed && !failure ? assertion : failure + }) + }, - this.failure = function() { - return inject(this.assertions, null, function(failure, assertion){ - return !assertion.passed && !failure ? assertion : failure - }) - } - - // Find all failing assertions - - this.failures = function() { - return inject(this.assertions, [], function(failures, assertion){ - if (!assertion.passed) failures.push(assertion) - return failures - }) - } + // Find all failing assertions - // Weither or not the spec passed + failures : function() { + return inject(this.assertions, [], function(failures, assertion){ + if (!assertion.passed) failures.push(assertion) + return failures + }) + }, - this.passed = function() { - return !this.failure() - } + // Weither or not the spec passed - // Weither or not the spec requires implementation (no assertions) + passed : function() { + return !this.failure() + }, - this.requiresImplementation = function() { - return this.assertions.length == 0 - } - - // Sprite based assertions graph - - this.assertionsGraph = function() { - return map(this.assertions, function(assertion){ - return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>' - }).join('') - } + // 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('') + } + }) }, // --- Methods /** + * 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 ' + print(actual) + ' to ' + + (negate ? 'not ' : '') + + name.replace(/_/g, ' ') + + ' ' + print.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) { - if ((value = query(key)) !== null) return value - else return JSpec.options[key] || null + return (value = query(key)) !== null ? value : + JSpec.options[key] || null }, /** * Generates a hash of the object passed. * @@ -471,13 +535,13 @@ * @api private */ match : function(actual, negate, name, expected) { if (typeof negate == 'string') negate = negate == 'should' ? false : true - matcher = new this.Matcher(name, this.matchers[name], actual, expected, negate) - this.currentSpec.assertions.push(matcher.exec()) - return matcher.result + assertion = new JSpec.Assertion(this.matchers[name], actual, expected, negate) + this.currentSpec.assertions.push(assertion.exec()) + return assertion.passed }, /** * Iterate an object, invoking the given callback. * @@ -491,11 +555,11 @@ if (typeof object == 'string') object = object.split(' ') for (key in object) { if (object.hasOwnProperty(key)) callback.length == 1 ? callback.call(JSpec, object[key]): - callback.call(JSpec, key, object[key]) + callback.call(JSpec, key, object[key]) } return JSpec }, /** @@ -510,11 +574,11 @@ inject : function(object, initial, callback) { each(object, function(key, value){ initial = callback.length == 2 ? callback.call(JSpec, initial, value): - callback.call(JSpec, initial, key, value) || initial + callback.call(JSpec, initial, key, value) || initial }) return initial }, /** @@ -529,10 +593,24 @@ strip : function(string, chars) { return string. replace(new RegExp('[' + (chars || '\\s') + ']*$'), ''). replace(new RegExp('^[' + (chars || '\\s') + ']*'), '') }, + + /** + * 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 + }) + }, /** * Map callback return values. * * @param {hash, array} object @@ -543,11 +621,11 @@ map : function(object, callback) { return inject(object, [], function(memo, key, value){ memo.push(callback.length == 1 ? callback.call(JSpec, value): - callback.call(JSpec, key, value)) + callback.call(JSpec, key, value)) }) }, /** * Returns true if the callback returns true at least once. @@ -561,11 +639,11 @@ any : function(object, callback) { return inject(object, false, function(state, key, value){ if (state) return true return callback.length == 1 ? callback.call(JSpec, value): - callback.call(JSpec, key, value) + callback.call(JSpec, key, value) }) }, /** * Define matchers. @@ -574,24 +652,41 @@ * @return {JSpec} * @api public */ addMatchers : function(matchers) { - each(matchers, function(name, body){ this.matchers[name] = body }) + each(matchers, function(name, body){ + this.addMatcher(name, body) + }) return this }, /** + * Define a matcher. + * + * @param {string} name + * @param {hash, function, string} body + * @return {JSpec} + * @api public + */ + + addMatcher : function(name, body) { + this.matchers[name] = this.normalizeMatcherMessage(this.normalizeMatcherBody(body)) + this.matchers[name].name = name + return this + }, + + /** * Add a root suite to JSpec. * * @param {string} description * @param {body} function * @return {JSpec} * @api public */ - addSuite : function(description, body) { + describe : function(description, body) { this.suites.push(new JSpec.Suite(description, body)) return this }, /** @@ -616,20 +711,20 @@ * @api private */ preprocess : function(input) { return input. - replace(/describe (.*?)$/m, 'JSpec.addSuite($1, function(){'). - replace(/describe (.*?)$/gm, 'this.addSuite($1, function(){'). - replace(/it (.*?)$/gm, 'this.addSpec($1, function(){'). + replace(/describe (.*?)$/m, 'JSpec.describe($1, function(){'). + replace(/describe (.*?)$/gm, 'this.describe($1, function(){'). + replace(/it (.*?)$/gm, 'this.it($1, function(){'). replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'this.addHook("$1", function(){'). replace(/end(?= |\n|$)/gm, '});'). - replace(/-{/g, 'function(){'). + replace(/-\{/g, 'function(){'). replace(/(\d+)\.\.(\d+)/g, function(_, a, b){ return range(a, b) }). replace(/([\s\(]+)\./gm, '$1this.'). replace(/\.should([_\.]not)?[_\.](\w+)(?: |$)(.*)$/gm, '.should$1_$2($3)'). - replace(/(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, 'JSpec.match($1, "$2", "$3", [$4]);') + replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 JSpec.match($2, "$3", "$4", [$5]);') }, /** * Create a range string which can be evaluated to a native array. * @@ -647,29 +742,30 @@ }, /** * Report on the results. * - * @return {JSpec} * @api public */ report : function() { this.options.formatter ? new this.options.formatter(this, this.options): - new this.formatters.DOM(this, this.options) - return this + new this.formatters.DOM(this, this.options) }, /** - * Run the spec suites. + * Run the spec suites. Options are merged + * with JSpec options when present. * + * @param {hash} options * @return {JSpec} * @api public */ - run : function() { + run : function(options) { + if (options) extend(this.options, options) if (option('profile')) console.group('Profile') each(this.suites, function(suite) { this.runSuite(suite) }) if (option('profile')) console.groupEnd() return this }, @@ -697,10 +793,22 @@ this.runSuite(suite) }) } return this }, + + /** + * 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 @@ -709,11 +817,12 @@ runSpec : function(spec) { this.currentSpec = spec this.stats.specs++ if (option('profile')) console.time(spec.description) - this.evalBody(spec.body, "Error in spec '" + spec.description + "': ") + try { this.evalBody(spec.body) } + catch (e) { fail(e) } if (option('profile')) console.timeEnd(spec.description) this.stats.assertions += spec.assertions.length }, /** @@ -724,28 +833,28 @@ * @api public */ requires : function(dependency, message) { try { eval(dependency) } - catch (e) { error('depends on ' + dependency + ' ' + (message || '')) } + 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 public + * @api private */ query : function(key, queryString) { - queryString = (queryString || window.location.search || '').substring(1) + 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 + return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value }) }, /** * Throw a JSpec related error. @@ -754,30 +863,83 @@ * @param {Exception} e * @api public */ error : function(message, e) { - throw 'jspec: ' + message + (e ? e.message : '') + ' near line ' + e.line + throw (message ? message : '') + e.toString() + + (e.line ? ' near line ' + e.line : '') + + (JSpec.file ? ' in ' + JSpec.file : '') }, + + /** + * Ad-hoc POST request for JSpec server usage. + * + * @param {string} url + * @param {string} data + * @api private + */ + + post : function(url, data) { + 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() { + JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures) + if ('close' in window) window.close() + }, + + /** + * Instantiate an XMLHttpRequest. + * + * @return {ActiveXObject, XMLHttpRequest} + * @api private + */ + + xhr : function() { + return window.ActiveXObject ? + 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 ('XMLHttpRequest' in this.main) { - request = new XMLHttpRequest + this.file = file + if (this.hasXhr()) { + request = this.xhr() request.open('GET', file, false) request.send(null) if (request.readyState == 4) return request.responseText } - else if ('load' in this.main) - load(file) // TODO: workaround for IO issue / preprocessing + else if ('readFile' in main) + return readFile(file) else error('cannot load ' + file) }, /** @@ -794,36 +956,42 @@ } } // --- Utility functions + main = this + puts = print map = JSpec.map any = JSpec.any last = JSpec.last + fail = JSpec.fail range = JSpec.range each = JSpec.each option = JSpec.option inject = JSpec.inject error = JSpec.error escape = JSpec.escape + extend = JSpec.extend print = JSpec.print hash = JSpec.hash query = JSpec.query strip = JSpec.strip + color = JSpec.color addMatchers = JSpec.addMatchers // --- Matchers addMatchers({ - be : "alias eql", 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", @@ -831,16 +999,18 @@ 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 : { match : function(actual, expected) { - if (actual.constructor == Array || actual.constructor == Object) return hash(actual) == hash(expected) - else return actual == expected - }}, + eql : function(actual, expected) { + return actual.constructor == Array || + actual instanceof Object ? + hash(actual) == hash(expected): + actual == expected + }, - include : { match : function(actual) { + include : function(actual) { for (state = true, i = 1; i < arguments.length; i++) { arg = arguments[i] switch (actual.constructor) { case String: case Number: @@ -858,46 +1028,53 @@ break } if (!state) return false } return true - }}, + }, - throw_error : { match : function(actual, expected) { + throw_error : function(actual, expected) { try { actual() } catch (e) { - if (expected == undefined) return true - else return expected.constructor == RegExp ? - expected.test(e) : e.toString() == expected + if (expected == undefined) return true + switch (expected.constructor) { + case RegExp: return expected.test(e) + case Function: return e instanceof expected + case String: return expected == e.toString() + } } - }}, + }, - have : { match : function(actual, length, property) { + have : function(actual, length, property) { return actual[property].length == length - }}, + }, - have_at_least : { match : function(actual, length, property) { + have_at_least : function(actual, length, property) { return actual[property].length >= length - }}, + }, - have_at_most : { match : function(actual, length, property) { + have_at_most :function(actual, length, property) { return actual[property].length <= length - }}, + }, - have_within : { match : function(actual, range, property) { + have_within : function(actual, range, property) { length = actual[property].length return length >= range.shift() && length <= range.pop() - }}, + }, - have_prop : { match : function(actual, property, value) { - if (actual[property] == null || typeof actual[property] == 'function') return false - return value == null ? true : JSpec.matchers['eql'].match(actual[property], value) - }}, + have_prop : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + JSpec.matchers.eql.match(actual[property], value) + }, - have_property : { match : function(actual, property, value) { - if (actual[property] == null || typeof actual[property] == 'function') return false - return value == null ? true : value === actual[property] - }} + have_property : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + value === actual[property] + } }) // --- Expose this.JSpec = JSpec