UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

327 lines (270 loc) 7.3 kB
// JSpec - Core // Copyright 2008 - 2009 TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed) // TODO: JSpec rename var Jspec = { version : '0.1.0', suites : {}, stats : { specs : 0, assertions : 0, failures : 0, passes : 0 }, // --- Matchers matchers : { eql : { match : function(expected, got) { return got == expected } }, equal : { match : function(expected, got) { return got === expected } } }, // --- Objects // TODO: stop using this.func ... use this.extend {} // TODO: benefit of using a context for 'this' arg ? .... messages even? Matcher : function (name, matcher, expected, got, negate) { var self = this // TODO: remove these since they are not used.. well some of them, other objects too this.name = name this.message = '' this.passed = false function generateMessage() { return got + ' should' + (negate ? ' not' : '') + ' ' + name + ' ' + expected } function setMessage() { if (typeof matcher.message == 'function') self.message = matcher.message(expected, got, negate) else self.message = generateMessage() } function pass() { setMessage() Jspec.stats.passes += 1 self.passed = true } function fail() { setMessage() self.message += ', but it did' + (negate ? '' : ' not') Jspec.stats.failures += 1 } this.match = function() { return matcher.match.call(got, expected.valueOf(), got.valueOf()) } this.passes = function() { return negate? !this.match() : this.match() } this.exec = function() { if (this.passes()) pass() else fail() return this } }, /** * Default formatter, outputting to the DOM. */ Formatter : function(results) { var markup = '', report = document.getElementById('jspec') if (!report) throw 'JSpec requires div#jspec to output its reports' markup += '<div id="jspec-report"><div class="heading">' + '<span class="passes">Passes: <em>' + results.stats.passes + '</em></span> ' + '<span class="failures">Failures: <em>' + results.stats.failures + '</em></span>' + '</div><div class="suites">' results.each(results.suites, function(description, suite){ markup += '<h2>' + description + '</h2>' results.each(suite.specs, function(i, spec){ // TODO: utilize matcher descriptions ... and auto-generate when not present // TODO: allow it to not have description // spec.failure().message var assertionCount = ' (<span class="assertion-count">' + spec.assertions.length + '</span>)' if (spec.requiresImplementation()) markup += '<p class="requires-implementation">' + spec.description + '</p>' else if (spec.passed()) markup += '<p class="pass">' + spec.description + assertionCount + '</p>' else markup += '<p class="fail">' + spec.description + assertionCount + '</p>' }) }) markup += '</div></div>' report.innerHTML = markup }, /** * Specification Suite block object. * * @param {string} description */ Suite : function(description) { this.specs = [] this.description = description this.addSpec = function(spec) { this.specs.push(spec) spec.suite = this } }, /** * Specification block object. * * @param {string} description * @param {string} body */ Spec : function(description, body) { this.body = body this.description = description this.assertions = [] this.failure = function() { var failure Jspec.each(this.assertions, function(i, assertion){ if (!assertion.passed) failure = assertion }) return failure } this.passed = function() { return !this.failure() } this.requiresImplementation = function() { return this.assertions.length == 0 } }, // --- Methods /** * Iterate an object, invoking the given callback. * * @param {object} object * @param {function} callback * @return {Type} */ each : function(object, callback) { for (var key in object) { if (typeof object[key] == 'function') continue callback.call(this, key, object[key]) } return this }, /** * Define matchers. * * @param {object} matchers * @return {Jspec} */ defineMatchers : function(matchers) { this.each(matchers, function(key, callbacks){ Object.prototype['should_' + key] = function(other) { var matcher = new Jspec.Matcher(key, matchers[key], this, other) Jspec.currentSpec.assertions.push(matcher.exec()) } Object.prototype['should_not_' + key] = function(other) { var matcher = new Jspec.Matcher(key, matchers[key], this, other, true) Jspec.currentSpec.assertions.push(matcher.exec()) } }) return this }, /** * Report on the results. * * @return {Jspec} */ report : function() { var formatter = new Jspec.Formatter(this) return this }, /** * Run the spec suites. * * @return {Jspec} */ run : function() { this.each(this.suites, function(description, suite) { this.runSuite(suite) }) return this }, /** * Run a suite. * * @param {Suite} suite * @return {Jspec} */ runSuite : function(suite) { this.each(suite.specs, function(i, spec) { this.currentSpec = spec this.stats.specs += 1 try { eval(spec.body) } catch(e) { throw "Error in spec '" + spec.description + "': " + e } this.stats.assertions += spec.assertions.length }) return this }, /** * Evaluate a string of jspec. * * @param {string} input * @return {Jspec} */ eval : function(input) { return this.parse(input) }, /** * Parse a string. * * @param {string} input * @return {Jspec} */ parse : function(input) { var describing = false, specing = false var token, describe, spec, body = [] var tokens = this.tokenize(input) while (tokens.length) { token = tokens.shift() switch (token) { case 'end': if (specing) { var newSpec = new Jspec.Spec(spec, body.join('')) this.suites[describe] = this.suites[describe] || new Jspec.Suite(describe) this.suites[describe].addSpec(newSpec) body = [], specing = false, spec = null } else if (describing) { describing = false, describe = null } break case 'describe': describing = true break case 'it': specing = true break } if (spec) { body.push(token) } else { if (/'.*?'/.test(token)) { if (specing) spec = token.replace(/'/g, '') else if (describing) describe = token.replace(/'/g, '') } } } return this }, /** * Tokenize a string. * * @param {string} input * @return {array} */ tokenize : function(input) { if (input.constructor == Array) return input var regexp = /it|describe|end|'.*?'|\n|./g return input.match(regexp) }, /** * Load a files contents. * * @param {string} file * @return {string} */ load : function(file) { var request = new XMLHttpRequest request.open('GET', file, false) request.send(null) return request.responseText }, /** * Evaluate, run, and report on the file passed. * * @param {string} file */ exec : function(file) { this.eval(this.load(file)).run().report() } } Jspec.defineMatchers(Jspec.matchers)