/v2/src/jsspec2.js
JavaScript | 686 lines | 446 code | 116 blank | 124 comment | 72 complexity | f064e7d8803b842d5c9ff407ba577963 MD5 | raw file
1/** 2 * @namespace Single namespace that contains all classes and functions of jsspec 3 */ 4var jsspec = {}; 5 6 7 8/** 9 * @class Base class to emulate classical class-based OOP 10 */ 11jsspec.Class = function() {}; 12jsspec.Class._initializing = false; 13jsspec.Class._fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 14 15/** 16 * @param {object} base Base class to be extended 17 * @return {jsspec.Class} Extended class instance 18 */ 19jsspec.Class.extend = function(base) { 20 // Inspired by http://ejohn.org/blog/simple-javascript-inheritance/ 21 var _super = this.prototype; 22 23 jsspec.Class._initialiing = true; 24 var prototype = new this(); 25 jsspec.Class._initialiing = false; 26 27 for(var name in base) { 28 prototype[name] = typeof base[name] == 'function' && 29 typeof _super[name] == 'function' && jsspec.Class._fnTest.test(base[name]) ? 30 (function(name, fn){ 31 return function() { 32 var tmp = this._super; 33 this._super = _super[name]; 34 var ret = fn.apply(this, arguments); 35 this._super = tmp; 36 return ret; 37 }; 38 })(name, base[name]) : 39 base[name]; 40 } 41 42 function Class() { 43 if ( !jsspec.Class._initializing && this.init ) this.init.apply(this, arguments); 44 } 45 46 Class.prototype = prototype; 47 Class.constructor = Class; 48 Class.extend = arguments.callee; 49 50 return Class; 51}; 52 53 54 55/** 56 * @class Encapsulates differences of various host environments 57 * @extends jsspec.Class 58 */ 59jsspec.HostEnvironment = jsspec.Class.extend(/** @lends jsspec.HostEnvironment.prototype */{ 60 /** 61 * Prints single line message to console 62 * 63 * @param {string} message A message to print 64 */ 65 log: function(message) {throw 'Not implemented';}, 66 67 /** 68 * @return {string} Short description of current host environment 69 */ 70 getDescription: function() {return 'Unknown environment'} 71}); 72 73/** 74 * Static factory 75 * 76 * @returns {jsspec.HostEnvironment} Platform specific instance 77 */ 78jsspec.HostEnvironment.getInstance = function() { 79 if(jsspec.root.navigator) { 80 return new jsspec.BrowserHostEnvironment(); 81 } else if(jsspec.root.load) { 82 return new jsspec.RhinoHostEnvironment(); 83 } else if(jsspec.root.WScript) { 84 return new jsspec.WScriptHostEnvironment(); 85 } 86} 87 88 89 90/** 91 * @class Browser host environment 92 * @extends jsspec.HostEnvironment 93 */ 94jsspec.BrowserHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.BrowserHostEnvironment.prototype */{ 95 log: function(message) { 96 jsspec.root.document.title = message; 97 98 var escaped = (message + '\n').replace(/</img, '<').replace(/\n/img, '<br />'); 99 jsspec.root.document.write(escaped); 100 }, 101 getDescription: function() { 102 return jsspec.root.navigator.userAgent; 103 }, 104 _findBasePath: function() { 105 var scripts = document.getElementsByTagName("script"); 106 for(var i = 0; i < scripts.length; i++) { 107 var script = scripts[i]; 108 if(script.src && script.src.match(/jsspec2\.js/i)) { 109 return script.src.match(/(.*\/)jsspec2\.js.*/i)[1]; 110 } 111 } 112 return './'; 113 } 114}); 115 116 117 118/** 119 * @class Rhino host environment 120 * @extends jsspec.HostEnvironment 121 */ 122jsspec.RhinoHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.RhinoHostEnvironment.prototype */{ 123 log: function(message) { 124 jsspec.root.print(message); 125 }, 126 getDescription: function() { 127 return 'Rhino (Java ' + jsspec.root.environment['java.version'] + ')'; 128 }, 129 _findBasePath: function() { 130 return jsspec.root.environment['user.dir'] + jsspec.root.environment['file.separator']; 131 } 132}); 133 134 135 136/** 137 * @class Windows Script host environment 138 * @extends jsspec.HostEnvironment 139 */ 140jsspec.WScriptHostEnvironment = jsspec.HostEnvironment.extend(/** @lends jsspec.WScriptHostEnvironment.prototype */{ 141 log: function(message) { 142 jsspec.root.WScript.StdOut.WriteLine(message); 143 }, 144 getDescription: function() { 145 return 'Windows Script Host ' + WScript.Version; 146 }, 147 _readFile: function(path) { 148 var fso = new jsspec.root.ActiveXObject('Scripting.FileSystemObject'); 149 var file; 150 try { 151 file = fso.OpenTextFile(path); 152 return file.ReadAll(); 153 } finally { 154 try {if(file) file.Close();} catch(ignored) {} 155 } 156 }, 157 _findBasePath: function() { 158 return '.'; 159 } 160}); 161 162 163 164/** 165 * @class Collection of assertion APIs 166 */ 167jsspec.Assertion = { 168 /** 169 * Makes an example fail unconditionally 170 * 171 * @param {string} [description] Optional description 172 */ 173 fail: function(description) { 174 throw new jsspec.ExpectationFailure(description || 'Failed'); 175 }, 176 177 /** 178 * Performs equality test 179 * 180 * @param {object} expected Expected value 181 * @param {object} actual Actual value 182 * @param {string} [description] Optional description 183 */ 184 assertEquals: function(expected, actual, description) { 185 var matcher = jsspec.Matcher.getInstance(expected, actual); 186 if(!matcher.matches()) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']'); 187 }, 188 189 /** 190 * Performs type test 191 * 192 * @param {string} expected Expected type 193 * @param {object} actual Actual object 194 * @param {string} [description] Optional description 195 */ 196 assertType: function(expected, actual, description) { 197 var type = jsspec.util.getType(actual); 198 if(expected !== type) throw new jsspec.ExpectationFailure((description || 'Type expectation failure') + '. Expected [' + expected + '] but [' + type + ']'); 199 }, 200 201 /** 202 * Checks if given value is true 203 * 204 * @param {boolean} actual Actual object 205 * @param {string} [description] Optional description 206 */ 207 assertTrue: function(actual, description) { 208 var expected = true; 209 if(expected !== actual) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']'); 210 }, 211 212 /** 213 * Checks if given value is false 214 * 215 * @param {boolean} actual Actual object 216 * @param {string} [description] Optional description 217 */ 218 assertFalse: function(actual, description) { 219 var expected = false; 220 if(expected !== actual) throw new jsspec.ExpectationFailure((description || 'Expectation failure') + '. Expected [' + expected + '] but [' + actual + ']'); 221 } 222}; 223 224 225 226/** 227 * @class Exception class to represent expectation failure (instead of error) 228 * @extends jsspec.Class 229 */ 230jsspec.ExpectationFailure = jsspec.Class.extend(/** @lends jsspec.ExpectationFailure.prototype */{ 231 /** 232 * @constructs 233 * @param {string} message An failure message 234 */ 235 init: function(message) { 236 this._message = message; 237 }, 238 toString: function() { 239 return this._message; 240 } 241}); 242 243 244 245/** 246 * @class Performs equality check for given objects 247 * @extends jsspec.Class 248 */ 249jsspec.Matcher = jsspec.Class.extend(/** @lends jsspec.Matcher.prototype */{ 250 /** 251 * @constructs 252 * @param {object} expected An expected object 253 * @param {object} actual An actual object 254 */ 255 init: function(expected, actual) { 256 this._expected = expected; 257 this._actual = actual; 258 }, 259 260 /** 261 * @returns {object} An expected object 262 */ 263 getExpected: function() {return this._expected;}, 264 265 /** 266 * @returns {object} An actual object 267 */ 268 getActual: function() {return this._actual;}, 269 270 /** 271 * @param {boolean} True if matches 272 */ 273 matches: function() {return this.getExpected() === this.getActual();} 274}); 275 276/** 277 * Returns appropriate jsspec.Matcher instance for given parameters' type 278 * 279 * @param {object} expected An expected object 280 * @param {object} actual An actual object 281 * @returns {jsspec.Matcher} An instance of jsspec.Matcher 282 */ 283jsspec.Matcher.getInstance = function(expected, actual) { 284 if(expected === null || expected === undefined) return new jsspec.Matcher(expected, actual); 285 286 var type = jsspec.util.getType(expected); 287 var clazz = null; 288 289 if('array' === type) { 290 clazz = jsspec.ArrayMatcher; 291 } else if('date' === type) { 292 clazz = jsspec.DateMatcher; 293 } else if('regexp' === type) { 294 clazz = jsspec.RegexpMatcher; 295 } else if('object' === type) { 296 clazz = jsspec.ObjectMatcher; 297 } else { // if string, boolean, number, function and anything else 298 clazz = jsspec.Matcher; 299 } 300 301 return new clazz(expected, actual); 302}; 303 304 305 306/** 307 * @class Performs equality check for two arrays 308 * @extends jsspec.Matcher 309 */ 310jsspec.ArrayMatcher = jsspec.Matcher.extend(/** @lends jsspec.ArrayMatcher.prototype */{ 311 matches: function() { 312 if(!this.getActual()) return false; 313 if(this.getExpected().length !== this.getActual().length) return false; 314 315 for(var i = 0; i < this.getExpected().length; i++) { 316 var expected = this.getExpected()[i]; 317 var actual = this.getActual()[i]; 318 if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false; 319 } 320 321 return true; 322 } 323}); 324 325 326 327/** 328 * @class Performs equality check for two date instances 329 * @extends jsspec.Matcher 330 */ 331jsspec.DateMatcher = jsspec.Matcher.extend(/** @lends jsspec.DateMatcher.prototype */{ 332 matches: function() { 333 if(!this.getActual()) return false; 334 return this.getExpected().getTime() === this.getActual().getTime(); 335 } 336}); 337 338 339 340/** 341 * @class Performs equality check for two regular expressions 342 * @extends jsspec.Matcher 343 */ 344jsspec.RegexpMatcher = jsspec.Matcher.extend(/** @lends jsspec.RegexpMatcher.prototype */{ 345 matches: function() { 346 if(!this.getActual()) return false; 347 return this.getExpected().source === this.getActual().source; 348 } 349}); 350 351 352 353/** 354 * @class Performs equality check for two objects 355 * @extends jsspec.Matcher 356 */ 357jsspec.ObjectMatcher = jsspec.Matcher.extend(/** @lends jsspec.ObjectMatcher.prototype */{ 358 matches: function() { 359 if(!this.getActual()) return false; 360 361 for(var key in this.getExpected()) { 362 var expected = this.getExpected()[key]; 363 var actual = this.getActual()[key]; 364 if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false; 365 } 366 367 for(var key in this.getActual()) { 368 var expected = this.getActual()[key]; 369 var actual = this.getExpected()[key]; 370 if(!jsspec.Matcher.getInstance(expected, actual).matches()) return false; 371 } 372 373 return true; 374 } 375}); 376 377 378 379jsspec.Example = jsspec.Class.extend({ 380 init: function(name, func) { 381 this._name = name; 382 this._func = func; 383 this._result = null; 384 }, 385 386 getName: function() {return this._name;}, 387 getFunction: function() {return this._func;}, 388 getResult: function() {return this._result;}, 389 390 run: function(reporter, context) { 391 reporter.onExampleStart(this); 392 393 var exception = null; 394 395 try { 396 this.getFunction().apply(context); 397 } catch(e) { 398 jsspec.host.log("{{{"); 399 for(var key in e) { 400 jsspec.host.log(" - " + key + ": " + e[key]); 401 } 402 jsspec.host.log("}}}"); 403 jsspec.host.log(" "); 404 exception = e; 405 } 406 407 this._result = new jsspec.Result(this, exception); 408 409 reporter.onExampleEnd(this); 410 } 411}); 412 413 414 415jsspec.ExampleSet = jsspec.Class.extend({ 416 init: function(name, examples) { 417 this._name = name; 418 this._examples = examples || []; 419 this._setup = jsspec._EMPTY_FUNCTION; 420 this._teardown = jsspec._EMPTY_FUNCTION; 421 }, 422 getName: function() {return this._name;}, 423 getSetup: function() {return this._setup;}, 424 getTeardown: function() {return this._teardown;}, 425 426 addExample: function(example) { 427 this._examples.push(example); 428 }, 429 addExamples: function(examples) { 430 for(var i = 0; i < examples.length; i++) { 431 this.addExample(examples[i]); 432 } 433 }, 434 setSetup: function(func) { 435 this._setup = func; 436 }, 437 setTeardown: function(func) { 438 this._teardown = func; 439 }, 440 getLength: function() { 441 return this._examples.length; 442 }, 443 getExampleAt: function(index) { 444 return this._examples[index]; 445 }, 446 run: function(reporter) { 447 reporter.onExampleSetStart(this); 448 449 for(var i = 0; i < this.getLength(); i++) { 450 var context = {}; 451 452 this.getSetup().apply(context); 453 this.getExampleAt(i).run(reporter, context); 454 this.getTeardown().apply(context); 455 } 456 457 reporter.onExampleSetEnd(this); 458 } 459}); 460 461 462jsspec.Result = jsspec.Class.extend({ 463 init: function(example, exception) { 464 this._example = example; 465 this._exception = exception; 466 }, 467 468 getExample: function() {return this._example;}, 469 getException: function() {return this._exception;}, 470 471 success: function() { 472 return !this.getException(); 473 }, 474 failure: function() { 475 return !this.success() && (this.getException() instanceof jsspec.ExpectationFailure); 476 }, 477 error: function() { 478 return !this.success() && !(this.getException() instanceof jsspec.ExpectationFailure); 479 } 480}); 481 482 483 484jsspec.Reporter = jsspec.Class.extend({ 485 init: function(host) { 486 this._host = host; 487 }, 488 onStart: function() {throw 'Not implemented';}, 489 onEnd: function() {throw 'Not implemented';}, 490 onExampleSetStart: function(exset) {throw 'Not implemented';}, 491 onExampleSetEnd: function(exset) {throw 'Not implemented';}, 492 onExampleStart: function(example) {throw 'Not implemented';}, 493 onExampleEnd: function(example) {throw 'Not implemented';} 494}); 495 496jsspec.Reporter.getInstance = function() { 497 if(jsspec.host instanceof jsspec.BrowserHostEnvironment) { 498// return new jsspec.HtmlReporter(jsspec.host); 499 return new jsspec.ConsoleReporter(jsspec.host); 500 } else { 501 return new jsspec.ConsoleReporter(jsspec.host); 502 } 503}; 504 505 506 507jsspec.DummyReporter = jsspec.Reporter.extend({ 508 init: function() { 509 this.log = []; 510 }, 511 onStart: function() { 512 this.log.push({op: 'onStart'}); 513 }, 514 onEnd: function() { 515 this.log.push({op: 'onEnd'}); 516 }, 517 onExampleSetStart: function(exset) { 518 this.log.push({op: 'onExampleSetStart', exset:exset.getName()}); 519 }, 520 onExampleSetEnd: function(exset) { 521 this.log.push({op: 'onExampleSetEnd', exset:exset.getName()}); 522 }, 523 onExampleStart: function(example) { 524 this.log.push({op: 'onExampleStart', example:example.getName()}); 525 }, 526 onExampleEnd: function(example) { 527 this.log.push({op: 'onExampleEnd', example:example.getName()}); 528 } 529}); 530 531 532 533jsspec.HtmlReporter = jsspec.Reporter.extend({ 534 init: function(host) { 535 this._super(host); 536 this._total = 0; 537 this._failures = 0; 538 this._errors = 0; 539 540 document.write('<h1>JSSpec</h1>'); 541 }, 542 onStart: function() { 543 }, 544 onEnd: function() { 545 }, 546 onExampleSetStart: function(exset) { 547 }, 548 onExampleSetEnd: function(exset) { 549 }, 550 onExampleStart: function(example) { 551 }, 552 onExampleEnd: function(example) { 553 this._total++; 554 555 var result = example.getResult(); 556 if(result.success()) return; 557 558 this._host.log('- ' + result.getException()); 559 if(result.failure()) { 560 this._failures++; 561 } else { 562 this._errors++; 563 } 564 } 565}); 566 567 568 569jsspec.ConsoleReporter = jsspec.Reporter.extend({ 570 init: function(host) { 571 this._super(host); 572 this._total = 0; 573 this._failures = 0; 574 this._errors = 0; 575 }, 576 onStart: function() { 577 this._host.log('JSSpec2 on ' + this._host.getDescription()); 578 this._host.log(''); 579 }, 580 onEnd: function() { 581 this._host.log('----'); 582 this._host.log('Total: ' + this._total + ', Failures: ' + this._failures + ', Errors: ' + this._errors + ''); 583 }, 584 onExampleSetStart: function(exset) { 585 this._host.log('[' + exset.getName() + ']'); 586 }, 587 onExampleSetEnd: function(exset) { 588 this._host.log(''); 589 }, 590 onExampleStart: function(example) { 591 this._host.log(example.getName()); 592 }, 593 onExampleEnd: function(example) { 594 this._total++; 595 596 var result = example.getResult(); 597 if(result.success()) return; 598 599 this._host.log('- ' + result.getException()); 600 if(result.failure()) { 601 this._failures++; 602 } else { 603 this._errors++; 604 } 605 } 606}); 607 608 609 610jsspec.util = { 611 getType: function(o) { 612 var ctor = o.constructor; 613 614 if(ctor == Array) { 615 return 'array'; 616 } else if(ctor == Date) { 617 return 'date'; 618 } else if(ctor == RegExp) { 619 return 'regexp'; 620 } else { 621 return typeof o; 622 } 623 } 624} 625 626 627jsspec.dsl = { 628 TDD: new (jsspec.Class.extend({ 629 init: function() { 630 this._current = null; 631 this._exsets = []; 632 }, 633 suite: function(name) { 634 this._current = new jsspec.ExampleSet(name); 635 this._exsets.push(this._current); 636 return this; 637 }, 638 test: function(name, func) { 639 if(!this._current) { 640 this.suite('Default example set'); 641 } 642 643 this._current.addExample(new jsspec.Example(name, func)); 644 return this; 645 }, 646 setup: function(func) { 647 if(!this._current) { 648 this.suite('Default example set'); 649 } 650 651 this._current.setSetup(func); 652 }, 653 teardown: function(func) { 654 if(!this._current) { 655 this.suite('Default example set'); 656 } 657 658 this._current.setTeardown(func); 659 }, 660 run: function() { 661 this._reporter = jsspec.Reporter.getInstance(); 662 663 this._reporter.onStart(); 664 665 for(var i = 0; i < this._exsets.length; i++) { 666 this._exsets[i].run(this._reporter); 667 } 668 669 this._reporter.onEnd(); 670 671 return this; 672 }, 673 fail: jsspec.Assertion.fail, 674 assertEquals: jsspec.Assertion.assertEquals, 675 assertType: jsspec.Assertion.assertType, 676 assertTrue: jsspec.Assertion.assertTrue, 677 assertFalse: jsspec.Assertion.assertFalse 678 })) 679}; 680 681 682 683 684jsspec.root = this; 685jsspec._EMPTY_FUNCTION = function() {} 686jsspec.host = jsspec.HostEnvironment.getInstance();