lib/jspec.js in visionmedia-jspec-1.1.7 vs lib/jspec.js in visionmedia-jspec-2.0.0

- old
+ new

@@ -3,22 +3,19 @@ (function(){ var JSpec = { - version : '1.1.7', - file : '', - suites : [], - matchers : {}, - stats : { specs : 0, assertions : 0, failures : 0, passes : 0 }, - options : { profile : false }, + version : '2.0.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. - * This allows specs and hooks to use the 'this' keyword in - * order to store variables, as well as allowing the context - * to provide helper methods or properties. * * Replace context simply by setting JSpec.context * to your own like below: * * JSpec.context = { foo : 'bar' } @@ -30,16 +27,44 @@ * * JSpec.context = null * */ - defaultContext : { - sandbox : function(name) { - sandbox = document.createElement('div') - sandbox.setAttribute('class', 'jspec-sandbox') - document.body.appendChild(sandbox) - return sandbox + 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 @@ -58,18 +83,25 @@ 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') + if (!report) throw 'JSpec 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">' - + + bodyContents = function(body) { + return JSpec. + escape(JSpec.contentsOf(body)). + replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }). + replace("\n", '<br/>') + } + 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){ @@ -78,11 +110,11 @@ 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>' - markup += '<tr class="body" style="display: none;"><td colspan="2">' + spec.body + '</td></tr>' + markup += '<tr class="body"><td colspan="2"><pre>' + bodyContents(spec.body) + '</pre></td></tr>' }) markup += '</tr>' } } @@ -92,13 +124,11 @@ if (suite.hasSuites()) renderSuites(suite.suites) }) } renderSuites(results.suites) - markup += '</table></div>' - report.innerHTML = markup }, /** * Terminal formatter. @@ -117,21 +147,21 @@ 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){ + 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 + else if (!spec.passed()) puts(color(' ' + spec.description, 'red') + assertionsGraph + - "\n" + indent(spec.failure().message) + "\n") + "\n" + indent(spec.failure().message) + "\n") }) puts('') } } @@ -156,11 +186,11 @@ console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures) renderSuite = function(suite) { if (suite.ran) { console.group(suite.description) - results.each(suite.specs, function(spec){ + each(suite.specs, function(spec){ assertionCount = spec.assertions.length + ':' if (spec.requiresImplementation()) console.warn(spec.description) else if (spec.passed()) console.log(assertionCount + ' ' + spec.description) @@ -188,48 +218,169 @@ passed : false, actual : actual, negate : negate, matcher : matcher, expected : expected, - record : function(result) { - result ? JSpec.stats.passes++ : JSpec.stats.failures++ + + // Report assertion results + + report : function() { + this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++ + return this }, - exec : function() { + // Run the assertion + + run : 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) + this.result = matcher.match.apply(JSpec, expected) + this.passed = negate ? !this.result : this.result if (!this.passed) this.message = matcher.message(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() { + methodString = 'expected ' + object.toString() + '.' + method + '()' + times = function(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 ' + print(this.expectedResult) + + ' but got ' + print(this.failingResult()) + + if (this.expectedArgs && this.anyArgsFail()) + this.message = methodString + ' to be called with ' + print.apply(this, this.expectedArgs) + + ' but was called with ' + print.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 - it : function(description, body) { + addSpec : function(description, body) { spec = new JSpec.Spec(description, body) this.specs.push(spec) + JSpec.stats.specs++ // TODO: abstract spec.suite = this }, // Add a hook to the suite @@ -237,23 +388,25 @@ this.hooks[hook].push(body) }, // Add a nested suite - describe : function(description, body) { + addSuite : function(description, body) { 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 '" + this.description + "': ") + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ") }) }, // Check if nested suites are present @@ -268,15 +421,13 @@ }, // Check if the entire suite passed passed : function() { - var passed = true - each(this.specs, function(spec){ - if (!spec.passed()) passed = false + return !any(this.specs, function(spec){ + return !spec.passed() }) - return passed } }) }, /** @@ -291,24 +442,31 @@ 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 inject(this.assertions, null, function(failure, assertion){ - return !assertion.passed && !failure ? assertion : failure + return find(this.assertions, function(assertion){ + return !assertion.passed }) }, // Find all failing assertions failures : function() { - return inject(this.assertions, [], function(failures, assertion){ - if (!assertion.passed) failures.push(assertion) - return failures + return select(this.assertions, function(assertion){ + return !assertion.passed }) }, // Weither or not the spec passed @@ -329,14 +487,108 @@ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>' }).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. ' + print(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) { + args = [] + for (i = 0; i < arguments.length; i++) args.push(arguments[i]) + return args.slice(offset || 0) + }, + + /** * Return ANSI-escaped colored string. * * @param {string} string * @param {string} color * @return {string} @@ -440,17 +692,18 @@ * @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 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() @@ -477,32 +730,33 @@ * @api public */ print : function(object) { if (arguments.length > 1) { - list = [] - for (i = 0; i < arguments.length; i++) list.push(print(arguments[i])) - return list.join(', ') + return map(argumentsToArray(arguments), function(arg){ + return print(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 ' + print(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 Array : - buff = '[' - each(object, function(v){ buff += ', ' + print(v) }) - return buff.replace('[,', '[') + ' ]' + case Array : + return inject(object, '[', function(b, v){ + return b + ', ' + print(v) + }).replace('[,', '[') + ' ]' case Object: - buff = '{' - each(object, function(k, v){ buff += ', ' + print(k) + ' : ' + print(v)}) - return buff.replace('{,', '{') + ' }' + return inject(object, '{', function(b, k, v) { + return b + ', ' + print(k) + ' : ' + print(v) + }).replace('{,', '{') + ' }' default: return escape(object.toString()) } }, @@ -513,17 +767,38 @@ * @return {string} * @api public */ escape : function(html) { - if (typeof html != 'string') return html - return 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) + * + * @param {mixed} actual + * @param {string} matcher + * @param {...} expected + * @return {mixed} + * @api private + */ + + does : function(actual, matcher, expected) { + assertion = new JSpec.Assertion(JSpec.matchers[matcher], actual, argumentsToArray(arguments, 2)) + return assertion.run().result + }, /** * Perform an assertion. * * expect(true).to('be', true) @@ -534,24 +809,25 @@ * @return {hash} * @api public */ expect : function(actual) { - assert = function(name, args, negate) { + assert = function(matcher, args, negate) { expected = [] for (i = 1; i < args.length; i++) expected.push(args[i]) - assertion = new JSpec.Assertion(JSpec.matchers[name], actual, expected, negate) - JSpec.currentSpec.assertions.push(assertion.exec()) - return assertion.passed + assertion = new JSpec.Assertion(matcher, actual, expected, negate) + if (matcher.defer) assertion.run() + else JSpec.currentSpec.assertions.push(assertion.run().report()) + return assertion.result } - to = function(name) { - return assert(name, arguments, false) + to = function(matcher) { + return assert(matcher, arguments, false) } - not_to = function(name) { - return assert(name, arguments, true) + not_to = function(matcher) { + return assert(matcher, arguments, true) } return { to : to, should : to, @@ -559,49 +835,10 @@ should_not : not_to } }, /** - * Iterate an object, invoking the given callback. - * - * @param {hash, array, string} object - * @param {function} callback - * @return {JSpec} - * @api public - */ - - each : function(object, callback) { - 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]) - } - return JSpec - }, - - /** - * Iterate with memo. - * - * @param {hash, array} object - * @param {object} initial - * @param {function} callback - * @return {object} - * @api public - */ - - 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 - }) - return initial - }, - - /** * Strim whitespace or chars. * * @param {string} string * @param {string} chars * @return {string} @@ -613,10 +850,25 @@ 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 @@ -625,11 +877,47 @@ extend : function(object, other) { each(other, function(property, value){ object[property] = value }) }, - + + /** + * Iterate an object, invoking the given callback. + * + * @param {hash, array, string} object + * @param {function} callback + * @return {JSpec} + * @api public + */ + + each : function(object, callback) { + if (typeof object == 'string') object = object.split(' ') + for (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 + }, + /** * Map callback return values. * * @param {hash, array} object * @param {function} callback @@ -637,77 +925,99 @@ * @api public */ 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)) + memo.push(callIterator(callback, key, value)) }) }, /** - * Returns true if the callback returns true at least once. + * Returns the first matching expression or null. * * @param {hash, array} object * @param {function} callback - * @return {bool} + * @return {mixed} * @api public */ 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) + return inject(object, null, function(state, key, value){ + return state ? state : + 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(memo, key, value){ + if (callIterator(callback, key, value)) + memo.push(value) + }) + }, /** * Define matchers. * * @param {hash} matchers - * @return {JSpec} * @api public */ addMatchers : function(matchers) { each(matchers, function(name, body){ - this.addMatcher(name, body) + JSpec.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 */ describe : function(description, body) { - this.suites.push(new JSpec.Suite(description, body)) - return this + suite = new JSpec.Suite(description, body) + 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 @@ -715,11 +1025,15 @@ * @return {Type} * @api private */ evalBody : function(body, errorMessage) { - try { body.call(this.context || this.defaultContext) } + dsl = this.DSL || this.DSLs.snake + matchers = this.matchers + context = this.context || this.defaultContext + contents = this.contentsOf(body) + try { eval('with (dsl){ with (context) { with (matchers) { ' + contents + ' }}}') } catch(e) { error(errorMessage, e) } }, /** * Pre-process a string of JSpec. @@ -729,20 +1043,18 @@ * @api private */ preprocess : function(input) { return input. - 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(/describe (.*?)$/gm, 'describe($1, function(){'). + replace(/ it (.*?)$/gm, ' it($1, function(){'). + replace(/^(?: *)(before_each|after_each|before|after)(?= |\n|$)/gm, 'JSpec.currentSuite.addHook("$1", function(){'). replace(/end(?= |\n|$)/gm, '});'). 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, '$1 expect($2).$3("$4", $5)'). + replace(/([\/ ]*)(.+?)\.(should(?:[_\.]not)?)[_\.](\w+)\((.*)\)$/gm, '$1 expect($2).$3($4, $5)'). replace(/, \)/gm, ')'). replace(/should\.not/gm, 'should_not') }, /** @@ -758,21 +1070,35 @@ 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 + ']' }, + + /** + * 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.options.formatter ? - new this.options.formatter(this, this.options): - new this.formatters.DOM(this, this.options) + 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. @@ -783,39 +1109,55 @@ */ run : function(options) { if (options) extend(this.options, options) if (option('profile')) console.group('Profile') - each(this.suites, function(suite) { this.runSuite(suite) }) + 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 - * @return {JSpec} * @api public */ runSuite : function(suite) { - suite.body() + this.currentSuite = suite + this.evalBody(suite.body) suite.ran = true suite.hook('before') each(suite.specs, function(spec) { - suite.hook('before_each') - this.runSpec(spec) - suite.hook('after_each') + JSpec.whenCurrentSpecIsFinished(function(){ + suite.hook('before_each') + JSpec.runSpec(spec) + suite.hook('after_each') + }) }) if (suite.hasSuites()) { each(suite.suites, function(suite) { - this.runSuite(suite) + JSpec.runSuite(suite) }) } suite.hook('after') - return this + this.stats.suitesFinished++ }, /** * Report a failure for the current spec. * @@ -835,15 +1177,16 @@ * @api public */ runSpec : function(spec) { this.currentSpec = spec - this.stats.specs++ 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. @@ -884,12 +1227,11 @@ * @api public */ error : function(message, e) { throw (message ? message : '') + e.toString() + - (e.line ? ' near line ' + e.line : '') + - (JSpec.file ? ' in ' + JSpec.file : '') + (e.line ? ' near line ' + e.line : '') }, /** * Ad-hoc POST request for JSpec server usage. * @@ -910,23 +1252,25 @@ * * @api private */ reportToServer : function() { - JSpec.post('http://localhost:4444', 'passes=' + JSpec.stats.passes + '&failures=' + JSpec.stats.failures) - if ('close' in window) window.close() + 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 window.ActiveXObject ? + return 'ActiveXObject' in main ? new ActiveXObject("Microsoft.XMLHTTP"): new XMLHttpRequest() }, /** @@ -947,11 +1291,10 @@ * @return {string} * @api public */ load : function(file) { - this.file = file if (this.hasXhr()) { request = this.xhr() request.open('GET', file, false) request.send(null) if (request.readyState == 4) return request.responseText @@ -969,37 +1312,42 @@ * @param {JSpec} * @api public */ exec : function(file) { - eval(this.preprocess(this.load(file))) + eval('with (JSpec){' + this.preprocess(this.load(file)) + '}') return this } } // --- Utility functions var main = this - var puts = print + var puts = main.print 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 print = JSpec.print var hash = JSpec.hash var query = JSpec.query var strip = JSpec.strip var color = JSpec.color - var expect = JSpec.expect + 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 : "===", @@ -1026,10 +1374,16 @@ 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) { @@ -1056,13 +1410,13 @@ throw_error : function(actual, expected) { try { actual() } catch (e) { 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() + case RegExp : return expected.test(e) + case Function : return e instanceof expected + case String : return expected == e.toString() } } }, have : function(actual, length, property) { @@ -1084,11 +1438,11 @@ 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) + does(actual[property], 'eql', value) }, have_property : function(actual, property, value) { return actual[property] == null || actual[property] instanceof Function ? false: @@ -1099,6 +1453,6 @@ // --- Expose this.JSpec = JSpec -})(); +})() \ No newline at end of file