/** * JSSpec * * Copyright 2007 Alan Kang * - mailto:jania902@gmail.com * - http://jania.pe.kr * * http://jania.pe.kr/aw/moin.cgi/JSSpec * * Dependencies: * - diff_match_patch.js ( http://code.google.com/p/google-diff-match-patch ) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ /** * Namespace */ var JSSpec = { specs: [], EMPTY_FUNCTION: function() {}, Browser: { // By Rendering Engines Trident: navigator.appName === "Microsoft Internet Explorer", Webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, KHTML: navigator.userAgent.indexOf('KHTML') !== -1, Presto: navigator.appName === "Opera", // By Platforms Mac: navigator.userAgent.indexOf("Macintosh") !== -1, Ubuntu: navigator.userAgent.indexOf('Ubuntu') !== -1, Win: navigator.userAgent.indexOf('Windows') !== -1, // By Browsers IE: navigator.appName === "Microsoft Internet Explorer", IE6: navigator.userAgent.indexOf('MSIE 6') !== -1, IE7: navigator.userAgent.indexOf('MSIE 7') !== -1, IE8: navigator.userAgent.indexOf('MSIE 8') !== -1, FF: navigator.userAgent.indexOf('Firefox') !== -1, FF2: navigator.userAgent.indexOf('Firefox/2') !== -1, FF3: navigator.userAgent.indexOf('Firefox/3') !== -1, Safari: navigator.userAgent.indexOf('Safari') !== -1 } }; /** * Executor */ JSSpec.Executor = function(target, onSuccess, onException) { this.target = target; this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; if(JSSpec.Browser.Trident) { // Exception handler for Trident. It helps to collect exact line number where exception occured. window.onerror = function(message, fileName, lineNumber) { var self = window._curExecutor; var ex = {message:message, fileName:fileName, lineNumber:lineNumber}; if(JSSpec._secondPass) { ex = self.mergeExceptions(JSSpec._assertionFailure, ex); delete JSSpec._secondPass; delete JSSpec._assertionFailure; ex.type = "failure"; self.onException(self, ex); } else if(JSSpec._assertionFailure) { JSSpec._secondPass = true; self.run(); } else { self.onException(self, ex); } return true; }; } }; JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) { var merged = { message:assertionFailure.message, fileName:normalException.fileName, lineNumber:normalException.lineNumber }; return merged; }; JSSpec.Executor.prototype.run = function() { var self = this; var target = this.target; var onSuccess = this.onSuccess; var onException = this.onException; window.setTimeout( function() { var result; if(JSSpec.Browser.Trident) { window._curExecutor = self; result = self.target(); self.onSuccess(self, result); } else { try { result = self.target(); self.onSuccess(self, result); } catch(ex) { if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line}; if(JSSpec._secondPass) { ex = self.mergeExceptions(JSSpec._assertionFailure, ex); delete JSSpec._secondPass; delete JSSpec._assertionFailure; ex.type = "failure"; self.onException(self, ex); } else if(JSSpec._assertionFailure) { JSSpec._secondPass = true; self.run(); } else { self.onException(self, ex); } } } }, 0 ); }; /** * CompositeExecutor composites one or more executors and execute them sequencially. */ JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) { this.queue = []; this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; this.continueOnException = !!continueOnException; }; JSSpec.CompositeExecutor.prototype.addFunction = function(func) { this.addExecutor(new JSSpec.Executor(func)); }; JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) { var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1]; if(last) { last.next = executor; } executor.parent = this; executor.onSuccessBackup = executor.onSuccess; executor.onSuccess = function(result) { this.onSuccessBackup(result); if(this.next) { this.next.run(); } else { this.parent.onSuccess(); } }; executor.onExceptionBackup = executor.onException; executor.onException = function(executor, ex) { this.onExceptionBackup(executor, ex); if(this.parent.continueOnException) { if(this.next) { this.next.run(); } else { this.parent.onSuccess(); } } else { this.parent.onException(executor, ex); } }; this.queue.push(executor); }; JSSpec.CompositeExecutor.prototype.run = function() { if(this.queue.length > 0) { this.queue[0].run(); } }; /** * Spec is a set of Examples in a specific context */ JSSpec.Spec = function(context, entries) { this.id = JSSpec.Spec.id++; this.context = context; this.url = location.href; this.filterEntriesByEmbeddedExpressions(entries); this.extractOutSpecialEntries(entries); this.examples = this.makeExamplesFromEntries(entries); this.examplesMap = this.makeMapFromExamples(this.examples); }; JSSpec.Spec.id = 0; JSSpec.Spec.prototype.getExamples = function() { return this.examples; }; JSSpec.Spec.prototype.hasException = function() { return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; }; JSSpec.Spec.prototype.getTotalFailures = function() { var examples = this.examples; var failures = 0; for(var i = 0; i < examples.length; i++) { if(examples[i].isFailure()) failures++; } return failures; }; JSSpec.Spec.prototype.getTotalErrors = function() { var examples = this.examples; var errors = 0; for(var i = 0; i < examples.length; i++) { if(examples[i].isError()) errors++; } return errors; }; JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) { var isTrue; for(name in entries) if(entries.hasOwnProperty(name)) { var m = name.match(/\[\[(.+)\]\]/); if(m && m[1]) { eval("isTrue = (" + m[1] + ")"); if(!isTrue) delete entries[name]; } } }; JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) { this.beforeEach = JSSpec.EMPTY_FUNCTION; this.beforeAll = JSSpec.EMPTY_FUNCTION; this.afterEach = JSSpec.EMPTY_FUNCTION; this.afterAll = JSSpec.EMPTY_FUNCTION; for(name in entries) if(entries.hasOwnProperty(name)) { if(name == 'before' || name == 'before each' || name == 'before_each') { this.beforeEach = entries[name]; } else if(name == 'before all' || name == 'before_all') { this.beforeAll = entries[name]; } else if(name == 'after' || name == 'after each' || name == 'after_each') { this.afterEach = entries[name]; } else if(name == 'after all' || name == 'after_all') { this.afterAll = entries[name]; } } delete entries['before']; delete entries['before each']; delete entries['before_each']; delete entries['before all']; delete entries['before_all']; delete entries['after']; delete entries['after each']; delete entries['after_each']; delete entries['after all']; delete entries['after_all']; }; JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) { var examples = []; for(name in entries) if(entries.hasOwnProperty(name)) { examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach)); } return examples; }; JSSpec.Spec.prototype.makeMapFromExamples = function(examples) { var map = {}; for(var i = 0; i < examples.length; i++) { var example = examples[i]; map[example.id] = examples[i]; } return map; }; JSSpec.Spec.prototype.getExampleById = function(id) { return this.examplesMap[id]; }; JSSpec.Spec.prototype.getExecutor = function() { var self = this; var onException = function(executor, ex) { self.exception = ex; }; var composite = new JSSpec.CompositeExecutor(); composite.addFunction(function() {JSSpec.log.onSpecStart(self);}); composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) { self.exception = ex; JSSpec.log.onSpecEnd(self); })); var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true); for(var i = 0; i < this.examples.length; i++) { exampleAndAfter.addExecutor(this.examples[i].getExecutor()); } exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException)); exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);}); composite.addExecutor(exampleAndAfter); return composite; }; /** * Example */ JSSpec.Example = function(name, target, before, after) { this.id = JSSpec.Example.id++; this.name = name; this.target = target; this.before = before; this.after = after; }; JSSpec.Example.id = 0; JSSpec.Example.prototype.isFailure = function() { return this.exception && this.exception.type == "failure"; }; JSSpec.Example.prototype.isError = function() { return this.exception && !this.exception.type; }; JSSpec.Example.prototype.getExecutor = function() { var self = this; var onException = function(executor, ex) { self.exception = ex; }; var composite = new JSSpec.CompositeExecutor(); composite.addFunction(function() {JSSpec.log.onExampleStart(self);}); composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) { self.exception = ex; JSSpec.log.onExampleEnd(self); })); var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true); targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException)); targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException)); targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);}); composite.addExecutor(targetAndAfter); return composite; }; /** * Runner */ JSSpec.Runner = function(specs, logger) { JSSpec.log = logger; this.totalExamples = 0; this.specs = []; this.specsMap = {}; this.addAllSpecs(specs); }; JSSpec.Runner.prototype.addAllSpecs = function(specs) { for(var i = 0; i < specs.length; i++) { this.addSpec(specs[i]); } }; JSSpec.Runner.prototype.addSpec = function(spec) { this.specs.push(spec); this.specsMap[spec.id] = spec; this.totalExamples += spec.getExamples().length; }; JSSpec.Runner.prototype.getSpecById = function(id) { return this.specsMap[id]; }; JSSpec.Runner.prototype.getSpecByContext = function(context) { for(var i = 0; i < this.specs.length; i++) { if(this.specs[i].context == context) return this.specs[i]; } return null; }; JSSpec.Runner.prototype.getSpecs = function() { return this.specs; }; JSSpec.Runner.prototype.hasException = function() { return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; }; JSSpec.Runner.prototype.getTotalFailures = function() { var specs = this.specs; var failures = 0; for(var i = 0; i < specs.length; i++) { failures += specs[i].getTotalFailures(); } return failures; }; JSSpec.Runner.prototype.getTotalErrors = function() { var specs = this.specs; var errors = 0; for(var i = 0; i < specs.length; i++) { errors += specs[i].getTotalErrors(); } return errors; }; JSSpec.Runner.prototype.run = function() { JSSpec.log.onRunnerStart(); var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true); for(var i = 0; i < this.specs.length; i++) { executor.addExecutor(this.specs[i].getExecutor()); } executor.run(); }; JSSpec.Runner.prototype.rerun = function(context) { JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log); JSSpec.runner.run(); }; /** * Logger */ JSSpec.Logger = function() { this.finishedExamples = 0; this.startedAt = null; }; JSSpec.Logger.prototype.onRunnerStart = function() { this._title = document.title; this.startedAt = new Date(); var container = document.getElementById('jsspec_container'); if(container) { container.innerHTML = ""; } else { container = document.createElement("DIV"); container.id = "jsspec_container"; document.body.appendChild(container); } var title = document.createElement("DIV"); title.id = "title"; title.innerHTML = [ '
'+JSSpec.util.escapeTags(example.target.toString())+'
');
sb.push('
" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "
actual value:
'); sb.push('' + JSSpec.util.inspect(this.actual, false, this.expected) + '
'); sb.push('should ' + (this.condition ? '' : 'not') + ' include:
'); sb.push('' + JSSpec.util.inspect(this.expected) + '
'); return sb.join(""); }; JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() { var matches; if(this.condition) { for(var i = 0; i < this.actual.length; i++) { matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); if(matches) { this.match = true; break; } } } else { this.match = true; for(var i = 0; i < this.actual.length; i++) { matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); if(matches) { this.match = false; break; } } } if(this.match) return ""; var sb = []; sb.push('actual value:
'); sb.push('' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '
'); sb.push('should ' + (this.condition ? '' : 'not') + ' include:
'); sb.push('' + JSSpec.util.inspect(this.expected) + '
'); return sb.join(""); }; /** * PropertyLengthMatcher */ JSSpec.PropertyLengthMatcher = function(num, property, o, condition) { this.num = num; this.o = o; this.property = property; if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') { this.property = 'length'; } this.condition = condition; this.conditionMet = function(x) { if(condition == 'exactly') return x.length == num; if(condition == 'at least') return x.length >= num; if(condition == 'at most') return x.length <= num; throw "Unknown condition '" + condition + "'"; }; this.match = false; this.explaination = this.makeExplain(); }; JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() { if(this.o._type == 'String' && this.property == 'length') { this.match = this.conditionMet(this.o); return this.match ? '' : this.makeExplainForString(); } else if(typeof this.o.length != 'undefined' && this.property == "length") { this.match = this.conditionMet(this.o); return this.match ? '' : this.makeExplainForArray(); } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) { this.match = this.conditionMet(this.o[this.property]); return this.match ? '' : this.makeExplainForObject(); } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) { this.match = false; return this.makeExplainForNoProperty(); } this.match = true; }; JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() { var sb = []; var exp = this.num == 0 ? 'be an empty string' : 'have ' + this.condition + ' ' + this.num + ' characters'; sb.push('actual value has ' + this.o.length + ' characters:
'); sb.push('' + JSSpec.util.inspect(this.o) + '
'); sb.push('but it should ' + exp + '.
'); return sb.join(""); }; JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() { var sb = []; var exp = this.num == 0 ? 'be an empty array' : 'have ' + this.condition + ' ' + this.num + ' items'; sb.push('actual value has ' + this.o.length + ' items:
'); sb.push('' + JSSpec.util.inspect(this.o) + '
'); sb.push('but it should ' + exp + '.
'); return sb.join(""); }; JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() { var sb = []; var exp = this.num == 0 ? 'be empty' : 'have ' + this.condition + ' ' + this.num + ' ' + this.property + '.'; sb.push('actual value has ' + this.o[this.property].length + ' ' + this.property + ':
'); sb.push('' + JSSpec.util.inspect(this.o, false, this.property) + '
'); sb.push('but it should ' + exp + '.
'); return sb.join(""); }; JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() { var sb = []; sb.push('actual value:
'); sb.push('' + JSSpec.util.inspect(this.o) + '
'); sb.push('should have ' + this.condition + ' ' + this.num + ' ' + this.property + ' but there\'s no such property.
'); return sb.join(""); }; JSSpec.PropertyLengthMatcher.prototype.matches = function() { return this.match; }; JSSpec.PropertyLengthMatcher.prototype.explain = function() { return this.explaination; }; JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) { return new JSSpec.PropertyLengthMatcher(num, property, o, condition); }; /** * EqualityMatcher */ JSSpec.EqualityMatcher = {}; JSSpec.EqualityMatcher.createInstance = function(expected, actual) { if(expected == null || actual == null) { return new JSSpec.NullEqualityMatcher(expected, actual); } else if(expected._type && expected._type == actual._type) { if(expected._type == "String") { return new JSSpec.StringEqualityMatcher(expected, actual); } else if(expected._type == "Date") { return new JSSpec.DateEqualityMatcher(expected, actual); } else if(expected._type == "Number") { return new JSSpec.NumberEqualityMatcher(expected, actual); } else if(expected._type == "Array") { return new JSSpec.ArrayEqualityMatcher(expected, actual); } else if(expected._type == "Boolean") { return new JSSpec.BooleanEqualityMatcher(expected, actual); } } return new JSSpec.ObjectEqualityMatcher(expected, actual); }; JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) { var sb = []; sb.push(actualDesc || 'actual value:
'); sb.push('' + JSSpec.util.inspect(actual) + '
'); sb.push(expectedDesc || 'should be:
'); sb.push('' + JSSpec.util.inspect(expected) + '
'); return sb.join(""); }; JSSpec.EqualityMatcher.diffExplain = function(expected, actual) { var sb = []; sb.push('diff:
'); sb.push(''); var dmp = new diff_match_patch(); var diff = dmp.diff_main(expected, actual); dmp.diff_cleanupEfficiency(diff); sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true)); sb.push('
'); return sb.join(""); }; /** * BooleanEqualityMatcher */ JSSpec.BooleanEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; }; JSSpec.BooleanEqualityMatcher.prototype.explain = function() { var sb = []; sb.push('actual value:
'); sb.push('' + JSSpec.util.inspect(this.actual) + '
'); sb.push('should be:
'); sb.push('' + JSSpec.util.inspect(this.expected) + '
'); return sb.join(""); }; JSSpec.BooleanEqualityMatcher.prototype.matches = function() { return this.expected == this.actual; }; /** * NullEqualityMatcher */ JSSpec.NullEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; }; JSSpec.NullEqualityMatcher.prototype.matches = function() { return this.expected == this.actual && typeof this.expected == typeof this.actual; }; JSSpec.NullEqualityMatcher.prototype.explain = function() { return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); }; JSSpec.DateEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; }; JSSpec.DateEqualityMatcher.prototype.matches = function() { return this.expected.getTime() == this.actual.getTime(); }; JSSpec.DateEqualityMatcher.prototype.explain = function() { var sb = []; sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString())); return sb.join(""); }; /** * ObjectEqualityMatcher */ JSSpec.ObjectEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; this.match = this.expected == this.actual; this.explaination = this.makeExplain(); }; JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match}; JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination}; JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() { if(this.expected == this.actual) { this.match = true; return ""; } if(JSSpec.util.isDomNode(this.expected)) { return this.makeExplainForDomNode(); } var key, expectedHasItem, actualHasItem; for(key in this.expected) { expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key); } for(key in this.actual) { expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key); } for(key in this.expected) { var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]); if(!matcher.matches()) return this.makeExplainForItemMismatch(key); } this.match = true; }; JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) { var sb = []; sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); return sb.join(""); }; JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) { var sb = []; sb.push('actual value has no item named ' + JSSpec.util.inspect(key) + '
'); sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); sb.push('but it should have the item whose value is ' + JSSpec.util.inspect(this.expected[key]) + '
'); sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); return sb.join(""); }; JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) { var sb = []; sb.push('actual value has item named ' + JSSpec.util.inspect(key) + '
'); sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); sb.push('but there should be no such item
'); sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); return sb.join(""); }; JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) { var sb = []; sb.push('actual value has an item named ' + JSSpec.util.inspect(key) + ' whose value is ' + JSSpec.util.inspect(this.actual[key]) + '
'); sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
'); sb.push('but it\'s value should be ' + JSSpec.util.inspect(this.expected[key]) + '
'); sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
'); return sb.join(""); }; /** * ArrayEqualityMatcher */ JSSpec.ArrayEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; this.match = this.expected == this.actual; this.explaination = this.makeExplain(); }; JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match}; JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination}; JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() { if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch(); for(var i = 0; i < this.expected.length; i++) { var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]); if(!matcher.matches()) return this.makeExplainForItemMismatch(i); } this.match = true; }; JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() { return JSSpec.EqualityMatcher.basicExplain( this.expected, this.actual, 'but it should be ' + this.expected.length + '
', 'actual value has ' + this.actual.length + ' items
' ); }; JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) { var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)]; var sb = []; sb.push('' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is ' + JSSpec.util.inspect(this.actual[index]) + ':
'); sb.push('' + JSSpec.util.inspect(this.actual, false, index) + '
'); sb.push('but it should be ' + JSSpec.util.inspect(this.expected[index]) + ':
'); sb.push('' + JSSpec.util.inspect(this.expected, false, index) + '
'); return sb.join(""); }; /** * NumberEqualityMatcher */ JSSpec.NumberEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; }; JSSpec.NumberEqualityMatcher.prototype.matches = function() { if(this.expected == this.actual) return true; }; JSSpec.NumberEqualityMatcher.prototype.explain = function() { return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); }; /** * StringEqualityMatcher */ JSSpec.StringEqualityMatcher = function(expected, actual) { this.expected = expected; this.actual = actual; }; JSSpec.StringEqualityMatcher.prototype.matches = function() { return this.expected == this.actual; }; JSSpec.StringEqualityMatcher.prototype.explain = function() { var sb = []; sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual)); return sb.join(""); }; /** * PatternMatcher */ JSSpec.PatternMatcher = function(actual, pattern, condition) { this.actual = actual; this.pattern = pattern; this.condition = condition; this.match = false; this.explaination = this.makeExplain(); }; JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) { return new JSSpec.PatternMatcher(actual, pattern, condition); }; JSSpec.PatternMatcher.prototype.makeExplain = function() { var sb; if(this.actual == null || this.actual._type != 'String') { sb = []; sb.push('actual value:
'); sb.push('' + JSSpec.util.inspect(this.actual) + '
'); sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
'); sb.push('' + JSSpec.util.inspect(this.pattern) + '
'); sb.push('but pattern matching cannot be performed.
'); return sb.join(""); } else { this.match = this.condition == !!this.actual.match(this.pattern); if(this.match) return ""; sb = []; sb.push('actual value:
'); sb.push('' + JSSpec.util.inspect(this.actual) + '
'); sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
'); sb.push('' + JSSpec.util.inspect(this.pattern) + '
'); return sb.join(""); } }; JSSpec.PatternMatcher.prototype.matches = function() { return this.match; }; JSSpec.PatternMatcher.prototype.explain = function() { return this.explaination; }; /** * Domain Specific Languages */ JSSpec.DSL = {}; JSSpec.DSL.forString = { normalizeHtml: function() { var html = this; // Uniformize quotation, turn tag names and attribute names into lower case html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) { var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase()) return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">" }); // validation self-closing tags html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) { return "<" + tag + attrs + " />"; }); // append semi-colon at the end of style value html = html.replace(/style="(.*?)"/mg, function(str, styleStr) { styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";" return 'style="' + styleStr + '"' }); // sort style entries // remove empty style attributes html = html.replace(/ style=";"/mg, ""); // remove new-lines html = html.replace(/\r/mg, ''); html = html.replace(/\n/mg, ''); return html; } }; JSSpec.DSL.describe = function(context, entries, base) { if(base) { for(var i = 0; i < JSSpec.specs.length; i++) { if(JSSpec.specs[i].context === base) { base = JSSpec.specs[i]; break; } } for(var i = 0; i < base.examples.length; i++) { var example = base.examples[i]; if(!entries[example.name]) entries[example.name] = example.target; } } JSSpec.specs.push(new JSSpec.Spec(context, entries)); }; JSSpec.DSL.value_of = function(target) { if(JSSpec._secondPass) return {}; var subject = new JSSpec.DSL.Subject(target); return subject; }; JSSpec.DSL.Subject = function(target) { this.target = target; }; JSSpec.DSL.Subject.prototype._type = 'Subject'; JSSpec.DSL.Subject.prototype.should_fail = function(message) { JSSpec._assertionFailure = {message:message}; throw JSSpec._assertionFailure; }; JSSpec.DSL.Subject.prototype.should_be = function(expected) { var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.should_not_be = function(expected) { // TODO JSSpec.EqualityMatcher should support 'condition' var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); if(matcher.matches()) { JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.should_be_empty = function() { this.should_have(0, this.getType() == 'String' ? 'characters' : 'items'); }; JSSpec.DSL.Subject.prototype.should_not_be_empty = function() { this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items'); }; JSSpec.DSL.Subject.prototype.should_be_true = function() { this.should_be(true); }; JSSpec.DSL.Subject.prototype.should_be_false = function() { this.should_be(false); }; JSSpec.DSL.Subject.prototype.should_be_null = function() { this.should_be(null); }; JSSpec.DSL.Subject.prototype.should_be_undefined = function() { this.should_be(undefined); }; JSSpec.DSL.Subject.prototype.should_not_be_null = function() { this.should_not_be(null); }; JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() { this.should_not_be(undefined); }; JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) { var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.should_have = function(num, property) { this._should_have(num, property, "exactly"); }; JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) { this._should_have(num, property, "exactly"); }; JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) { this._should_have(num, property, "at least"); }; JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) { this._should_have(num, property, "at most"); }; JSSpec.DSL.Subject.prototype.should_include = function(expected) { var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.should_not_include = function(expected) { var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.should_match = function(pattern) { var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } } JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) { var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false); if(!matcher.matches()) { JSSpec._assertionFailure = {message:matcher.explain()}; throw JSSpec._assertionFailure; } }; JSSpec.DSL.Subject.prototype.getType = function() { if(typeof this.target == 'undefined') { return 'undefined'; } else if(this.target == null) { return 'null'; } else if(this.target._type) { return this.target._type; } else if(JSSpec.util.isDomNode(this.target)) { return 'DomNode'; } else { return 'object'; } }; /** * Utilities */ JSSpec.util = { escapeTags: function(string) { return string.replace(//img, '>'); }, escapeMetastring: function(string) { return string.replace(/\r/img, '\\r').replace(/\n/img, '\\n').replace(/\¶\;\