PageRenderTime 329ms CodeModel.GetById 72ms app.highlight 148ms RepoModel.GetById 101ms app.codeStats 1ms

/v2/src/jsspec2.js

http://jsspec.googlecode.com/
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, '&lt;').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();