diff --git a/test.v1.0.0.js b/test.v1.0.0.js new file mode 100644 index 0000000..d261b8c --- /dev/null +++ b/test.v1.0.0.js @@ -0,0 +1,923 @@ +(function(window) { + +var testit = function() { + /** + * group class, which will contain tests + * In addition, it will be used for wrapping some wrong code from falling. + * @constructor + * @private + * @attribute {String} type type of object ('group' or 'test') + * @attribute {String} name name of group + * @attribute {String} status indicate results of all test in group ('pass','fail','error') + * @attribute {String} comment text specified by user + * @attribute {Error} error contain error object if some of tests throw it + * @attribute {Number} time time in ms spend on code in group + * @attribute {Object} result counters for tests and groups + * @attribute {array} stack array of tests and groups + */ + var group = function() { + this.type = 'group'; + this.name = undefined; + this.status = undefined; + this.comment = undefined; + this.error = undefined; + this.time = 0; + this.result = { + pass: 0, + fail: 0, + error: 0, + total: 0 + }; + this.stack = []; + } + + /** + * test class, which will contain result and some more info about one test + * @constructor + * @private + * @attribute {String} type type of object ('group' or 'test') + * @attribute {String} status indicate results of test ('pass','fail','error') + * @attribute {String} comment text specified by user + * @attribute {String} description text generated by script + * @attribute {Error} error contain error object if test can throw it without falling + * @attribute {Number} time time in ms spend on test + * @attribute {Array} argument all received arguments + */ + var test = function() { + this.type = 'test'; + this.status = undefined; + this.comment = undefined; + this.description = undefined; + this.error = undefined; + this.time = new Date().getTime(); + this.argument = []; + } + + /** + * main group + * @public + * @type {group} + */ + var root = new group(); + this.root = root; + root.name = 'root'; + root.time = new Date().getTime(); + + /** + * make new instace of group, fill it, add it to previous group.stack, fill some values in previous group + * @private + * @chainable + * @param {String} name name of new group + * @param {Function} fun function witch will be tryed to execute (commonly consist of tests and other groups) + * @return {Object} test with link + */ + var _makeGroup = function(name,fun) { + switch (arguments.length) { + case 1 : { + for (i in root.stack) { + if (root.stack[i].type !== 'group') continue; + if (root.stack[i].name === name) { + return Object.create(this,{link:{value:root.stack[i]}}); + } + } + } break; + case 2 : break; + default : throw new RangeError("too much arguments"); + } + + /** get timestamp */ + var time = new Date().getTime(); + + /** making a new root, to provide chaining */ + var oldRootChain = root; + root = (this.link)? this.link : root; + /** var for the new instance of group */ + var newgroup; + /** identify new group */ + var groupAlreadyExist = false; + /** find group in current-level stack */ + for (i in root.stack) { + if (root.stack[i].type !== 'group') continue; + if (root.stack[i].name === name) { + newgroup = root.stack[i]; + groupAlreadyExist = true; + break; + } + } + if (!groupAlreadyExist) newgroup = new group(); + newgroup.name = name; + + /** add backlink to provide trek back */ + newgroup.linkBack = root; + + /** set to pass as default. it's may be changed in some next lines */ + var oldstatus; + if (groupAlreadyExist) oldstatus = newgroup.status; + newgroup.status ='pass'; + + + /** + * making a new root, to provide nesting + * Nested tests and groups will us it, like first one use root. + */ + var oldRootNest = root; + root = newgroup; + /** + * try to execute code with tests and other groups in it + * This part provide nesting. + */ + try{ + fun(); + } catch(e) { + newgroup.status = 'error'; + var errorObject = {}; + generateError(e,errorObject); + + newgroup.error = errorObject; + } + /** + * reverse inheritance of status + * If some of deep nested test will 'fail', root will be 'fail' too. + * More info in updateStatus() comments. + */ + oldRootNest.status = updateStatus(oldRootNest.status,root.status); + oldRootChain.status = updateStatus(oldRootChain.status,root.status); + + /** take back old root */ + root = oldRootNest; + + /** take back old root */ + root = oldRootChain; + + + /** update time */ + newgroup.time += new Date().getTime() - time; + + + /** finally place this group into previous level stack (if it's a new group) */ + if (!groupAlreadyExist) root.stack.push(newgroup); + + /** update counters */ + updateCounters(newgroup); + + /** return testit with link to this group to provide chaining */ + return Object.create(this,{link:{value:newgroup}}); + } + /** + * public interface for _makeGroup + * @public + * @example + * test.group('name of group',function(){ + * test.it('nested test'); + * test.group('nested group',function(){ + * test.it('deep nested test'); + * }); + * }); + */ + this.group = _makeGroup; + + /** + * basic test. Make new instance of test, fill it, add it to previous group.stack, fill some values in previous group + * @private + * @chainable + * @param {Multiple} a @required first argument, which will check for truth it only transmitted + * @param {Multiple} b second argument which will compared with a if transmitted + * @return {Object} test with link + */ + var _it = function(a,b) { + /** + * making a new instance of test + * Most of code in this function will manipulate whis it. + */ + var newtest = new test(); + + /** + * fill newtest.argument with arguments + * (arguments is array-like object, but not array. So i can't just newtest.argument = newtest.argument.concat(arguments); or newtest.argument = arguments) + */ + for (i in arguments) { + newtest.argument.push(arguments[i]); + } + /** try to figure out what kind of test expected */ + switch (arguments.length) { + /** in case of no arguments - throw Reference error */ + case 0 : { + newtest.status = 'error'; + var e = new RangeError("at least one argument expected"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } break; + /** if there only one argument - test it for truth */ + case 1 : { + testNonFalse(newtest,[a]); + } break; + /** if there are two arguments - test equalence between them */ + case 2 : { + testEquivalence(newtest,[a,b]); + } break; + /** otherwise throw Range error */ + default : { + newtest.status = 'error'; + var e = new RangeError("too much arguments"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } + } + + /** update counters of contained object */ + updateCounters(root); + + /** reverse inheritance of status */ + root.status = updateStatus(root.status,newtest.status); + + /** finally place this test into container stack */ + root.stack.push(newtest); + + /** return testit with link to this test to provide chaining */ + return Object.create(this,{link:{value:newtest}}); + } + /** + * public interface for _it() + * @public + * @example + * test.it(someThing); + * test.it(myFunction()); + * test.it(myVar>5); + * test.it(myVar,mySecondVar); + */ + this.it = _it; + + /** + * test array of values for non-false + * @private + * @chainable + * @param {Array} args array of values which will be tested + * @return {Object} test with link + */ + var _them = function(args) { + /** + * making a new instance of test + * Most of code in this function will manipulate whis it. + */ + var newtest = new test(); + + for (i in arguments) { + newtest.argument.push(arguments[i]); + } + + /** throw error if argument is not array */ + if (_typeof(args) !== 'Array') { + newtest.status = 'error'; + var e = new RangeError("test.them expects to receive an array"); + var errorObject = {}; + generateError(e,errorObject); + + newtest.error = errorObject; + } else { + testNonFalse(newtest,args); + } + + /** update counters of contained object */ + updateCounters(root); + + /** reverse inheritance of status */ + root.status = updateStatus(root.status,newtest.status); + + /** finally place this test into container stack */ + root.stack.push(newtest); + + /** return testit with link to this test to provide chaining */ + return Object.create(this,{link:{value:newtest}}); + } + /** + * public interface for _them() + * @public + * @example + * test.them([1,'a',true,window]); + */ + this.them = _them; + + /** + * test type of first argument (value) to be equal to secon argument + * @private + * @chainable + * @param {Multiple} value will be tested + * @param {[type]} type type to compare with + */ + var _type = function(value,type) { + /** + * making a new instance of test + * Most of code in this function will manipulate whis it. + */ + var newtest = new test(); + /** + * fill newtest.argument with arguments + * (arguments is array-like object, but not array. So i can't just newtest.argument = newtest.argument.concat(arguments); or newtest.argument = arguments) + */ + for (i in arguments) { + newtest.argument.push(arguments[i]); + } + /** throw error if there are not 2 arguments */ + if (arguments.length!==2) { + newtest.status = 'error'; + var e = new RangeError("test.type expect two arguments"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else if (_typeof(type) !== 'String') { + newtest.status = 'error'; + var e = new TypeError("second argument must be a String"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else { + testType(newtest,[value],type); + } + + /** update counters of contained object */ + updateCounters(root); + + // /** reverse inheritance of status */ + root.status = updateStatus(root.status,newtest.status); + + // /** finally place this test into container stack */ + // console.log(newtest); + root.stack.push(newtest); + + /** return testit with link to this test to provide chaining */ + return Object.create(this,{link:{value:newtest}}); + } + /** + * public interface for _type() + * @public + * @example + * test.type('asd','String'); + */ + this.type = _type; + + /** + * compare types of all values in args between each other and 'type' if defined + * @private + * @chainable + * @param {Array} args array of values which will be tested + * @param {String} type type which will be compared whith + */ + var _types = function(args,type) { + /** + * making a new instance of test + * Most of code in this function will manipulate whis it. + */ + var newtest = new test(); + /** + * fill newtest.argument with arguments + * (arguments is array-like object, but not array. So i can't just newtest.argument = newtest.argument.concat(arguments); or newtest.argument = arguments) + */ + for (i in arguments) { + newtest.argument.push(arguments[i]); + } + /** throw error if there are not 2 arguments */ + if (arguments.length>2) { + newtest.status = 'error'; + var e = new RangeError("test.types expect maximum of two arguments"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else if (_typeof(args) !== 'Array') { + newtest.status = 'error'; + var e = new TypeError("test.types expect array in first argument"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else if (type) { + if (_typeof(type) !== 'String') { + newtest.status = 'error'; + var e = new TypeError("second argument must be a String"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else { + testType(newtest,args,type); + } + } else if (args.length<2) { + newtest.status = 'error'; + var e = new RangeError("test.types expect array with minimum 2 values, if second argument not defined"); + var errorObject = {}; + generateError(e,errorObject); + newtest.error = errorObject; + } else { + testType(newtest,args); + } + + /** update counters of contained object */ + updateCounters(root); + + // /** reverse inheritance of status */ + root.status = updateStatus(root.status,newtest.status); + + // /** finally place this test into container stack */ + // console.log(newtest); + root.stack.push(newtest); + + /** return testit with link to this test to provide chaining */ + return Object.create(this,{link:{value:newtest}}); + } + /** + * public interface for _types + * @public + * @example + * test.type([1,2,3],'Number'); + */ + this.types = _types; + + /** + * Test types of all values in args + * @private + * @param {Objcet} test will be updated + * @param {Array} args consist values which types will be tested + * @param {[type]} type consist type which will be compared with + */ + var testType = function(test,args,type) { + test.description = 'type of argument is '; + + if (!type) type = _typeof(args[0]); + + for (arg in args) { + if (_typeof(args[arg]).toLowerCase() !== type.toLowerCase()) { + test.description += 'not '+type; + test.status = 'fail'; + return + } + } + + test.description += type; + test.status = 'pass'; + } + + /** + * Test all values in args for non-false + * @private + * @param {Objcet} test will be updated + * @param {Array} args consist values which will be tested + */ + var testNonFalse = function(test,args) { + /** use different text when one and multiple values are tested */ + test.description = (args.length===1)? 'argument is not ':'arguments is not '; + + /** test every value in args */ + for (arg in args) { + if (!args[arg]) { + test.description += 'true'; + test.status = 'fail'; + return; + } + } + + /** if code not stopped in previous step, test passed */ + test.description += 'false'; + test.status = 'pass'; + } + + /** + * Test all values in args for equivalence + * @private + * @param {Objcet} test will be updated + * @param {Array} args consist values which will be tested + */ + var testEquivalence = function(test,args) { + /** first value will be used as model in comparissons */ + var model = args.shift(); + + /** compare all types of values in args with type of model */ + var type = _typeof(model); + for (arg in args) { + if (_typeof(args[arg]) !== type) { + test.status = 'fail'; + test.description = 'arguments has different types'; + return; + } + } + + /** compare all values in args with model */ + for (arg in args) { + if (!deepCompare(model,args[arg])) { + test.description = 'arguments are not equal'; + test.status = 'fail'; + return; + } + } + + /** if code not stopped earlier, test passed */ + test.description = 'arguments are equal'; + test.status = 'pass'; + } + + /** update counters of contained object */ + var updateCounters = function(link) { + link.result = { + pass: 0, + fail: 0, + error: 0, + total: 0 + }; + + for (i in link.stack) { + link.result.total++; + switch (link.stack[i].status) { + case 'pass' : { + link.result.pass++; + } break; + case 'fail' : { + link.result.fail++; + } break; + case 'error' : { + link.result.error++; + } break; + }; + }; + + if (link.linkBack) { + updateCounters(link.linkBack); + } + } + + /** + * add comment for the linked test or group + * @private + * @chainable + * @type {Function} + * @param {String} text user defined text, which will be used as a comment + */ + var _comment = function(text) { + /** add comment, if there are something can be commented */ + if (!this.link) throw new ReferenceError('comment can only be used in testit chain'); + this.link.comment = text; + + return this; + } + /** + * public interface for _comment() + * @public + * @example + * test.group('group name',function(){ + * test + * .it(someThing) + * .comment('comment to test'); + * }).comment('comment to group'); + */ + this.comment = _comment; + + /** + * try to execute functions in arguments, depend on test|group result + * @private + * @chainable + * @param {Function} pass function to execute if test|group passed + * @param {Function} fail function to execute if test|group failed + * @param {Function} error function to execute if test|group cause error + */ + var _callback = function(pass,fail,error) { + if (!this.link) throw new ReferenceError('callback can only be used in testit chain'); + if (this.link.status === 'pass' && _typeof(pass) === 'Function' ) try {pass();} catch(e) {throw e;} + if (this.link.status === 'fail' && _typeof(fail) === 'Function' ) try {fail();} catch(e) {throw e;} + if (this.link.status === 'error' && _typeof(error) === 'Function' ) try {error();} catch(e) {throw e;} + + return this; + } + /** + * public interface for _callback() + * @public + * @example + * test.it(someThing).callback( + * function() {...} // - will be execute if test passed + * ,function() {...} // - will be execute if test failed + * ,function() {...} // - will be execute if test error + * ); + */ + this.callback = _callback; + + /** + * Final chain-link: will return result of test or group + * @private + * @return {boolean} true - if test or group passed, false - otherwise. + */ + var _result = function() { + if (this.link) { + return (this.link.status == 'pass')? true : false; + } + return undefined; + } + /** + * public interface for _result() + * @public + * @example + * var testResult = test.it(undefined).comment('comment to test').result(); // testResult === false + */ + this.result = _result; + + /** + * Final chain-link: will return arguments of test (not of group!) + * @private + * @return single argument or array of arguments + */ + var _arguments = function() { + if (this.link) { + if (this.link.type!=='test') return TypeError('groups does not return arguments'); + return (this.link.argument.length===1)? this.link.argument[0] : this.link.argument; + } + return undefined; + } + /** + * public interface for _arguments() + * @public + * @example + * var testArguments = test.it('single').comment('comment to test').arguments(); // testArguments === 'single' + * testArguments = test.it('first','second').comment('comment to test').arguments(); // testArguments === ['first','second'] + */ + this.arguments = _arguments; + + + /** + * apply last stuff and display results + * type {Function} + * @private + */ + var _done = function(obj) { + /** update time in root */ + root.time = new Date().getTime() - root.time; + + /** display root */ + // console.dir(root); + _printConsole(root); + } + /** + * public interface for _done() + * @type {Function} + * @public + * @example + * test.it(1); + * test.it(2); + * test.it(3); + * + * test.done(); + */ + this.done = _done; + + /** + * pritty display group or test in browser dev console + * @private + * @param {Object} obj group or test to display + */ + var _printConsole = function(obj) { + + /** colors for console.log %c */ + var green = "color: green", + red = "color: red;", + orange = "color: orange", + blue = "color: blue", + normal = "color: normal; font-weight:normal;"; + + /** Try to figure out what type of object display and open group */ + switch (obj.type) { + case 'group' : { + /** some difference depends on status */ + switch (obj.status) { + /** if object passed - make collapsed group*/ + case 'pass' : { + console.groupCollapsed("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s" + ,obj.name,green,obj.status,normal + ,green,obj.result.pass,normal + ,red,obj.result.fail,normal + ,orange,obj.result.error,normal + ,blue,obj.time,normal,((obj.comment)?obj.comment:'')); + } break; + case 'fail' : { + console.group("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s" + ,obj.name,red,obj.status,normal + ,green,obj.result.pass,normal + ,red,obj.result.fail,normal + ,orange,obj.result.error,normal + ,blue,obj.time,normal,((obj.comment)?obj.comment:'')); + } break; + case 'error' : { + console.group("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s" + ,obj.name,orange,obj.status,normal + ,green,obj.result.pass,normal + ,red,obj.result.fail,normal + ,orange,obj.result.error,normal + ,blue,obj.time,normal,((obj.comment)?obj.comment:'')); + } break; + /** if status is not defined - display error; finish displaying */ + default : { + console.error("No status in object %s",obj.name); + return false; + } + } + + /** display description if defined */ + if (obj.description) { + console.log(obj.description); + } + + /** + * display all tests and groups in stack + * It will make new levels of group, if there are groups in stack. + */ + for (i in obj.stack) { + _printConsole(obj.stack[i]); + } + + /** display error if defined */ + if (obj.error) { + // console.error(obj.error); + console.group('%c%s%c: %s',orange,obj.error.type,normal,obj.error.message); + console.log(obj.error.stack); + console.dir(obj.error.error); + console.groupEnd(); + } + + /** close opened group (current level) */ + console.groupEnd(); + + } break; + case 'test' : { + /** display different results, depend on status */ + switch (obj.status) { + case 'pass' : { + /** if pass - collaps group*/ + console.groupCollapsed("%cpass%c: %s",green,normal,(obj.comment)?obj.comment:''); + } break; + case 'fail' : { + console.group("%cfail%c: %s",red,normal,(obj.comment)?obj.comment:''); + } break; + case 'error' : { + console.group("%cerror%c: %s",orange,normal,(obj.comment)?obj.comment:''); + } break; + } + if (obj.description) console.log(obj.description); + /** display error if defined */ + if (obj.error) { + // console.error(obj.error); + console.group('%c%s%c: %s',orange,obj.error.type,normal,obj.error.message); + console.log(obj.error.stack); + console.dir(obj.error.error); + console.groupEnd(); + } + console.log(obj.argument); + console.groupEnd(); + } break; + } + } + /** + * public interface for _printConsole + * @type {Function} + * @public + * @example + * test.ptint(test.root); + */ + this.print = _printConsole; + + /** + * determinate type of argument + * More powerfull then typeof(). + * @private + * @return {String} type name of argument + * undefined, if type was not determinated + */ + var _typeof = function (argument) { + var type; + try { + switch (argument.constructor) { + case Array : type='Array';break; + case Boolean : type='Boolean';break; + case Date : type='Date';break; + case Error : type='Error';break; + case EvalError : type='EvalError';break; + case Function : type='Function';break; + // case Math : type='math';break; + case Number : {type=(isNaN(argument))?'NaN':'Number';}break; + case Object : type='Object';break; + case RangeError : type='RangeError';break; + case ReferenceError : type='ReferenceError';break; + case RegExp : type='RegExp';break; + case String : type='String';break; + case SyntaxError : type='SyntaxError';break; + case TypeError : type='TypeError';break; + case URIError : type='URIError';break; + case Window : type='Window';break; + case HTMLDocument : type='HTML';break; + case NodeList : type='NodeList';break; + default : { + if (typeof argument === 'object' + && argument.toString().indexOf('HTML') !== -1) { + type = 'HTML'; + } else { + type = undefined; + } + } + } + } catch (e) { + type = (argument === null)? 'null' : typeof argument; + } + return type; + } + /** + * public interface for _typeof + * @public + * @example + * test.typeof(myVar); + */ + this.typeof = _typeof; + + /** + * public interface for getTrace(error) + * @public + * @example + * test.trace(); + */ + this.trace = getTrace; + + + +} + +/** + * figure out what status will be used + * Depends on significanse: + * More significant -> less significant. + * error -> fail -> pass -> undefined + * @param {String} oldstatus first compared status + * @param {String} newstatus second compared status + * @return {String} status which will be set + */ +function updateStatus(oldstatus,newstatus) { + if (oldstatus===undefined) return newstatus; + if (newstatus===undefined) return oldstatus; + if (oldstatus==='error' || newstatus==='error') return 'error'; + if (oldstatus==='fail' || newstatus==='fail') return 'fail'; + return 'pass'; +} + +/** + * make more understandable error object + * @type {Object} + * @param {Error} error basic error + * @param {Object} object understandable error object + */ +function generateError(error,object) { + /** + * understandable error object + * @property {Error} error consist basic error + * @property {String} type type of error + * @property {String} message message from basic property + * @property {String} stack some kind of result of trace() + */ + object.error = error; + object.type = test.typeof(error); + object.message = error.message; + object.stack = getTrace(error); +} + +/** + * returns a list of functions that have been performed to call the current line + * @param {Error} error if setted, trace will be based on it stack + * @return {String} list of functions joined by "\n"; + */ +function getTrace(error) { + if (!error) error = new Error(); + var stack = ''; + error.stack.split(/[\n]/).forEach(function(i,n){ + var addToStack = true; + /** take off empty strings (FireBug) */ + if (i==='') addToStack = false; + /** take off Errors (Chrome) */ + if (i.indexOf(test.typeof(error))!==-1) addToStack = false; + /** take of reference to this function */ + if (i.indexOf('getTrace')!==-1) addToStack = false; + /** take off any references to testit methods */ + for (prop in test) { + if (i.indexOf('[as '+prop+']')!==-1) addToStack = false; + } + /** fill the stack */ + if (addToStack) { + stack += (stack)?'\n':''; + stack += i.replace(/((\s+at\s+)|(^@))/,''); + } + }) + return stack; +} + +/** + * Compare any type of variables + * @return {Boolean} result of comparison + * {@link http://stackoverflow.com/a/1144249/1771942} + */ +function deepCompare(){function c(d,e){var f;if(isNaN(d)&&isNaN(e)&&"number"==typeof d&&"number"==typeof e)return!0;if(d===e)return!0;if("function"==typeof d&&"function"==typeof e||d instanceof Date&&e instanceof Date||d instanceof RegExp&&e instanceof RegExp||d instanceof String&&e instanceof String||d instanceof Number&&e instanceof Number)return d.toString()===e.toString();if(!(d instanceof Object&&e instanceof Object))return!1;if(d.isPrototypeOf(e)||e.isPrototypeOf(d))return!1;if(d.constructor!==e.constructor)return!1;if(d.prototype!==e.prototype)return!1;if(a.indexOf(d)>-1||b.indexOf(e)>-1)return!1;for(f in e){if(e.hasOwnProperty(f)!==d.hasOwnProperty(f))return!1;if(typeof e[f]!=typeof d[f])return!1}for(f in d){if(e.hasOwnProperty(f)!==d.hasOwnProperty(f))return!1;if(typeof e[f]!=typeof d[f])return!1;switch(typeof d[f]){case"object":case"function":if(a.push(d),b.push(e),!c(d[f],e[f]))return!1;a.pop(),b.pop();break;default:if(d[f]!==e[f])return!1}}return!0}var a,b;if(arguments.length<1)return!0;for(var d=1,e=arguments.length;e>d;d++)if(a=[],b=[],!c(arguments[0],arguments[d]))return!1;return!0} + +/** + * make new instance of testit + * Make it availible from outside. + */ +window.test = new testit(); + +})(window) \ No newline at end of file diff --git a/testit.v1.0.0.min.js b/testit.v1.0.0.min.js new file mode 100644 index 0000000..e02d118 --- /dev/null +++ b/testit.v1.0.0.min.js @@ -0,0 +1 @@ +!function(a){function c(a,b){return void 0===a?b:void 0===b?a:"error"===a||"error"===b?"error":"fail"===a||"fail"===b?"fail":"pass"}function d(a,b){b.error=a,b.type=test.typeof(a),b.message=a.message,b.stack=e(a)}function e(a){a||(a=new Error);var b="";return a.stack.split(/[\n]/).forEach(function(c){var e=!0;""===c&&(e=!1),-1!==c.indexOf(test.typeof(a))&&(e=!1),-1!==c.indexOf("getTrace")&&(e=!1);for(prop in test)-1!==c.indexOf("[as "+prop+"]")&&(e=!1);e&&(b+=b?"\n":"",b+=c.replace(/((\s+at\s+)|(^@))/,""))}),b}function f(){function a(d,e){var f;if(isNaN(d)&&isNaN(e)&&"number"==typeof d&&"number"==typeof e)return!0;if(d===e)return!0;if("function"==typeof d&&"function"==typeof e||d instanceof Date&&e instanceof Date||d instanceof RegExp&&e instanceof RegExp||d instanceof String&&e instanceof String||d instanceof Number&&e instanceof Number)return d.toString()===e.toString();if(!(d instanceof Object&&e instanceof Object))return!1;if(d.isPrototypeOf(e)||e.isPrototypeOf(d))return!1;if(d.constructor!==e.constructor)return!1;if(d.prototype!==e.prototype)return!1;if(b.indexOf(d)>-1||c.indexOf(e)>-1)return!1;for(f in e){if(e.hasOwnProperty(f)!==d.hasOwnProperty(f))return!1;if(typeof e[f]!=typeof d[f])return!1}for(f in d){if(e.hasOwnProperty(f)!==d.hasOwnProperty(f))return!1;if(typeof e[f]!=typeof d[f])return!1;switch(typeof d[f]){case"object":case"function":if(b.push(d),c.push(e),!a(d[f],e[f]))return!1;b.pop(),c.pop();break;default:if(d[f]!==e[f])return!1}}return!0}var b,c;if(arguments.length<1)return!0;for(var d=1,e=arguments.length;e>d;d++)if(b=[],c=[],!a(arguments[0],arguments[d]))return!1;return!0}var b=function(){var a=function(){this.type="group",this.name=void 0,this.status=void 0,this.comment=void 0,this.error=void 0,this.time=0,this.result={pass:0,fail:0,error:0,total:0},this.stack=[]},b=function(){this.type="test",this.status=void 0,this.comment=void 0,this.description=void 0,this.error=void 0,this.time=(new Date).getTime(),this.argument=[]},g=new a;this.root=g,g.name="root",g.time=(new Date).getTime();var h=function(b,e){switch(arguments.length){case 1:for(i in g.stack)if("group"===g.stack[i].type&&g.stack[i].name===b)return Object.create(this,{link:{value:g.stack[i]}});break;case 2:break;default:throw new RangeError("too much arguments")}var f=(new Date).getTime(),h=g;g=this.link?this.link:g;var j,k=!1;for(i in g.stack)if("group"===g.stack[i].type&&g.stack[i].name===b){j=g.stack[i],k=!0;break}k||(j=new a),j.name=b,j.linkBack=g;var l;k&&(l=j.status),j.status="pass";var m=g;g=j;try{e()}catch(n){j.status="error";var o={};d(n,o),j.error=o}return m.status=c(m.status,g.status),h.status=c(h.status,g.status),g=m,g=h,j.time+=(new Date).getTime()-f,k||g.stack.push(j),q(j),Object.create(this,{link:{value:j}})};this.group=h;var j=function(a,e){var f=new b;for(i in arguments)f.argument.push(arguments[i]);switch(arguments.length){case 0:f.status="error";var h=new RangeError("at least one argument expected"),j={};d(h,j),f.error=j;break;case 1:o(f,[a]);break;case 2:p(f,[a,e]);break;default:f.status="error";var h=new RangeError("too much arguments"),j={};d(h,j),f.error=j}return q(g),g.status=c(g.status,f.status),g.stack.push(f),Object.create(this,{link:{value:f}})};this.it=j;var k=function(a){var e=new b;for(i in arguments)e.argument.push(arguments[i]);if("Array"!==x(a)){e.status="error";var f=new RangeError("test.them expects to receive an array"),h={};d(f,h),e.error=h}else o(e,a);return q(g),g.status=c(g.status,e.status),g.stack.push(e),Object.create(this,{link:{value:e}})};this.them=k;var l=function(a,e){var f=new b;for(i in arguments)f.argument.push(arguments[i]);if(2!==arguments.length){f.status="error";var h=new RangeError("test.type expect two arguments"),j={};d(h,j),f.error=j}else if("String"!==x(e)){f.status="error";var h=new TypeError("second argument must be a String"),j={};d(h,j),f.error=j}else n(f,[a],e);return q(g),g.status=c(g.status,f.status),g.stack.push(f),Object.create(this,{link:{value:f}})};this.type=l;var m=function(a,e){var f=new b;for(i in arguments)f.argument.push(arguments[i]);if(arguments.length>2){f.status="error";var h=new RangeError("test.types expect maximum of two arguments"),j={};d(h,j),f.error=j}else if("Array"!==x(a)){f.status="error";var h=new TypeError("test.types expect array in first argument"),j={};d(h,j),f.error=j}else if(e)if("String"!==x(e)){f.status="error";var h=new TypeError("second argument must be a String"),j={};d(h,j),f.error=j}else n(f,a,e);else if(a.length<2){f.status="error";var h=new RangeError("test.types expect array with minimum 2 values, if second argument not defined"),j={};d(h,j),f.error=j}else n(f,a);return q(g),g.status=c(g.status,f.status),g.stack.push(f),Object.create(this,{link:{value:f}})};this.types=m;var n=function(a,b,c){a.description="type of argument is ",c||(c=x(b[0]));for(arg in b)if(x(b[arg]).toLowerCase()!==c.toLowerCase())return a.description+="not "+c,a.status="fail",void 0;a.description+=c,a.status="pass"},o=function(a,b){a.description=1===b.length?"argument is not ":"arguments is not ";for(arg in b)if(!b[arg])return a.description+="true",a.status="fail",void 0;a.description+="false",a.status="pass"},p=function(a,b){var c=b.shift(),d=x(c);for(arg in b)if(x(b[arg])!==d)return a.status="fail",a.description="arguments has different types",void 0;for(arg in b)if(!f(c,b[arg]))return a.description="arguments are not equal",a.status="fail",void 0;a.description="arguments are equal",a.status="pass"},q=function(a){a.result={pass:0,fail:0,error:0,total:0};for(i in a.stack)switch(a.result.total++,a.stack[i].status){case"pass":a.result.pass++;break;case"fail":a.result.fail++;break;case"error":a.result.error++}a.linkBack&&q(a.linkBack)},r=function(a){if(!this.link)throw new ReferenceError("comment can only be used in testit chain");return this.link.comment=a,this};this.comment=r;var s=function(a,b,c){if(!this.link)throw new ReferenceError("callback can only be used in testit chain");if("pass"===this.link.status&&"Function"===x(a))try{a()}catch(d){throw d}if("fail"===this.link.status&&"Function"===x(b))try{b()}catch(d){throw d}if("error"===this.link.status&&"Function"===x(c))try{c()}catch(d){throw d}return this};this.callback=s;var t=function(){return this.link?"pass"==this.link.status?!0:!1:void 0};this.result=t;var u=function(){return this.link?"test"!==this.link.type?TypeError("groups does not return arguments"):1===this.link.argument.length?this.link.argument[0]:this.link.argument:void 0};this.arguments=u;var v=function(){g.time=(new Date).getTime()-g.time,w(g)};this.done=v;var w=function(a){var b="color: green",c="color: red;",d="color: orange",e="color: blue",f="color: normal; font-weight:normal;";switch(a.type){case"group":switch(a.status){case"pass":console.groupCollapsed("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s",a.name,b,a.status,f,b,a.result.pass,f,c,a.result.fail,f,d,a.result.error,f,e,a.time,f,a.comment?a.comment:"");break;case"fail":console.group("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s",a.name,c,a.status,f,b,a.result.pass,f,c,a.result.fail,f,d,a.result.error,f,e,a.time,f,a.comment?a.comment:"");break;case"error":console.group("%s - %c%s%c - %c%d%c/%c%d%c/%c%d%c (%c%d%c ms) %s",a.name,d,a.status,f,b,a.result.pass,f,c,a.result.fail,f,d,a.result.error,f,e,a.time,f,a.comment?a.comment:"");break;default:return console.error("No status in object %s",a.name),!1}a.description&&console.log(a.description);for(i in a.stack)w(a.stack[i]);a.error&&(console.group("%c%s%c: %s",d,a.error.type,f,a.error.message),console.log(a.error.stack),console.dir(a.error.error),console.groupEnd()),console.groupEnd();break;case"test":switch(a.status){case"pass":console.groupCollapsed("%cpass%c: %s",b,f,a.comment?a.comment:"");break;case"fail":console.group("%cfail%c: %s",c,f,a.comment?a.comment:"");break;case"error":console.group("%cerror%c: %s",d,f,a.comment?a.comment:"")}a.description&&console.log(a.description),a.error&&(console.group("%c%s%c: %s",d,a.error.type,f,a.error.message),console.log(a.error.stack),console.dir(a.error.error),console.groupEnd()),console.log(a.argument),console.groupEnd()}};this.print=w;var x=function(a){var b;try{switch(a.constructor){case Array:b="Array";break;case Boolean:b="Boolean";break;case Date:b="Date";break;case Error:b="Error";break;case EvalError:b="EvalError";break;case Function:b="Function";break;case Number:b=isNaN(a)?"NaN":"Number";break;case Object:b="Object";break;case RangeError:b="RangeError";break;case ReferenceError:b="ReferenceError";break;case RegExp:b="RegExp";break;case String:b="String";break;case SyntaxError:b="SyntaxError";break;case TypeError:b="TypeError";break;case URIError:b="URIError";break;case Window:b="Window";break;case HTMLDocument:b="HTML";break;case NodeList:b="NodeList";break;default:b="object"==typeof a&&-1!==a.toString().indexOf("HTML")?"HTML":void 0}}catch(c){b=null===a?"null":typeof a}return b};this.typeof=x,this.trace=e};a.test=new b}(window); \ No newline at end of file