综合众人的意见,此版本做了许多改进,如对注释抽取的优化,增加对script标签的支持,实时性的测试等等。
// dom.abut v2 (annotations-based unit testing by 司徒正美) // http://www.cnblogs.com/rubylouvre/archive/2010/11/08/1868638.html (function(){ //ecma262新扩展 if(!Object.keys){ var _dontEnum = ['propertyIsEnumerable', 'isPrototypeOf','hasOwnProperty','toLocaleString', 'toString', 'valueOf', 'constructor']; for (var i in { toString: 1 }) _dontEnum = false; Object.keys = function(obj){//ecma262v5 15.2.3.14 var result = [],dontEnum = _dontEnum,length = dontEnum.length; for(var key in obj ) if(obj.hasOwnProperty(key)){ result.push(key) } if(dontEnum){ while(length){ key = dontEnum[--length]; if(obj.hasOwnProperty(key)){ result.push(key); } } } return result; } } if(!String.prototype.trim){ String.prototype.trim = function(){ return this.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); } } if(!String.prototype.quote){ String.prototype.quote = (function () { var meta = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, reg = /[\\\"\x00-\x1f]/g, regFn = function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }; return function(){ return '"' + this.replace(reg, regFn) + '"'; } })(); } var addEvent = (function () { if (document.addEventListener) { return function (el, type, fn) { el.addEventListener(type, fn, false); }; } else { return function (el, type, fn) { el.attachEvent('on' + type, function () { return fn.call(el, window.event); }); } } })(); var applyIf = function(target,source){ for(var name in source) if(!target[name] ){ target[name] = source[name]; } return target; } //释出命名空间对象 window.dom = window.dom || {}; applyIf(dom,{ // http://www.cnblogs.com/rubylouvre/archive/2010/01/20/1652646.html type : (function(){ var reg = /^(\w)/, regFn = function($,$1){ return $1.toUpperCase() }, to_s = Object.prototype.toString; return function(obj,str){ var result = (typeof obj).replace(reg,regFn); if(result === 'Object' || (result === 'Function' && obj.exec) ){//safari chrome中 type /i/ 为function if(obj===null) result = 'Null';//Object,Function,Null,Undefined,Window,Arguments等等都为其构造器名称 else if(obj.window==obj) result = 'Window'; else if(obj.callee) result = 'Arguments'; else if(obj.nodeType === 9) result = 'Document'; else if(obj.nodeName) result = (obj.nodeName+'').replace('#',''); //处理元素节点 else if(!obj.constructor || !(obj instanceof Object)){ if("send" in obj && "setRequestHeader" in obj){//处理IE5-8的宿主对象与节点集合 result = "XMLHttpRequest" }else if("length" in obj && "item" in obj){ result = "namedItem" in obj ? 'HTMLCollection' :'NodeList'; }else{ result = 'Unknown'; } }else result = to_s.call(obj).slice(8,-1); } if(result === "Number" && isNaN(obj)) result = "NaN"; //safari chrome中 对 HTMLCollection与NodeList的to_s都为 "NodeList",此bug暂时无解 if(str){ return str === result; } return result; } })(), oneObject : function(array,val){ var result = {},value = val !== void 0 ? val :1; for(var i=0,n=array.length;i < n;i++) result[array[i]] = value; return result; } }); //http://www.cnblogs.com/rubylouvre/archive/2010/04/20/1716486.html (function(w,s){ s = ["XMLHttpRequest", "ActiveXObject('Msxml2.XMLHTTP.6.0')", "ActiveXObject('Msxml2.XMLHTTP.3.0')", "ActiveXObject('Msxml2.XMLHTTP')", "ActiveXObject('Microsoft.XMLHTTP')"]; if( !-[1,] && w.ScriptEngineMinorVersion() === 7 && location.protocol === "file:"){ s.shift(); } for(var i = 0 ,el;el=s[i++];){ try{ if(eval("new "+el)){ dom.xhr = new Function( "return new "+el); break; } }catch(e){} } })(window); var rcomments = /\/\/([^\r\n]+)|\/\*([\s\S]+?)\*\//gm; var rstar = /^(\s*)\*/gm; var r$$$$ = /(?:^|\s+)\${4}(\d+)(\w*)\(([^\r\n]+)\);?/g; var rwell = /[\/]{2,}\s*#{4}(\w*)\(([^\r\n]+)\)/; var rdelimiter = /\${4}|<{4}/; var ropacity = /opacity:\s*(\d?\.\d+)/g; var fns = { ok:";\nabut.ok", eq:";\nabut.eq", same:";\nabut.same", log:";\nabut.log" } //只有以下方法才进行测试统计 var countOne = dom.oneObject(["ok","eq","same",""]); //构建闭包的开头部分 var startClosure = function(arrayIndex){ return "closures["+ arrayIndex +"] = function(){\n var abut = window.dom.abut\n" } //构建闭包的结束部分 var endClosure = function(arrayIndex,lineNumber){ return "};\nclosures["+ arrayIndex+"].lineNumber = "+lineNumber+";\n"; } //针对一条测试注释的小型闭包 var smartClosure = function(str,arr,obj){ var temp = ""; str.replace(r$$$$,function($,$1,$2,$3){ var fn = fns[$2] || fns.ok, testCode = fn + "("+$3+");\n"; temp += startClosure(obj.arrayIndex)+ fn +".lineNumber = " + $1 + fn + ".testCode = " + testCode.slice(1,-2).quote() + testCode + endClosure(obj.arrayIndex,$1); if(countOne[$2]) obj.count++; obj.arrayIndex++; }); arr.push(temp ) } //针对多条测试注释的大型闭包 var bigClosure= function(str,arr,obj){ var lineNumber; str = str.replace(/^\d+/,function(str){ lineNumber = parseInt(str,10) ; return "" }); str = str.trim().replace(r$$$$,function($,$1,$2,$3){ if(countOne[$2]) obj.count ++; var fn = fns[$2] || fns.ok, testCode = fn + "("+$3+");\n"; return fn +".lineNumber = " + $1 + fn + ".testCode = " + testCode.slice(1,-2).quote() + testCode; }); var temp = startClosure(obj.arrayIndex) + str + endClosure(obj.arrayIndex,lineNumber); obj.arrayIndex++ arr.push(temp); } var modifyCode = function(method,textCode,lineNumber,obj){ if(countOne[method]) obj.count ++; dom.abut.isModify = true; var fn = fns[method] || fns.ok; return "try{\n var abut = dom.abut;"+ fn+".lineNumber = " + lineNumber +";"+ fn+".testCode = " + textCode.quote() +";"+ fn+"("+textCode+");\n"+ "}catch(e){\n"+ 'dom.but.render("dom-abut-unpass","第'+lineNumber+'行测试代码执行失败");\n}'; } var cleanCode = function (source,obj) { var lines = source.split( /\r?\n/) ,line ; for(var i=0,n = lines.length; i < n ;i++){ line = lines[i].trim(); if(rwell.test(line)){ lines[i] = line.replace(rwell,function($,$1,$2){ return modifyCode($1,$2,i,obj); }) }else{ lines[i] = lines[i].replace(rdelimiter,function(str){ return str + (i+1) }); } } return lines.join('\n'); }; var evalCode = function(source,target){ var abut = dom.abut; abut.ULID = "abut-"+(new Date-0).toString(36); abut.time = 0; abut.isModify = false; var obj = dom.oneObject(["arrayIndex","count"],0), uneval = cleanCode(source,obj), arr = dom.getComments(uneval).trim().split("<<<<"), i=0, n=arr.length, els,segment, resolving= ["var closures = window.dom.abut.closures = [];\n"]; while(i < n){ segment = arr[i++]; els = segment.split(">>>>"); if(segment.indexOf(">>>>") !== -1){//这里不使用el.length === 2是为了避开IE的split bug bigClosure(els[0],resolving,obj); if(els[1]){ smartClosure(els[1],resolving,obj); } }else{ smartClosure(els[0],resolving,obj); } } //构筑单元测试系统的UI var UL = document.createElement("UL"); abut.el = UL; target.appendChild(UL); UL.className ="dom-abut-result"; abut.render("dom-abut-title",'一共有'+obj.count+'个测试'); abut.recoder = document.getElementById( abut.ULID); if(!arguments.callee.first){//保证这里的代码只执行一次 arguments.callee.first = true; addEvent(target,"click",function(e){ var target = e.target || e.srcElement; if(target.className ==="dom-abut-slide"){ var blockquote = target.parentNode.getElementsByTagName("blockquote")[0]; if(blockquote){ blockquote.style.display = !!(blockquote.offsetHeight || blockquote.offestWidth) ? "none": "block"; } } }); //添加样式 dom.addSheet(".dom-abut-result {\ border:5px solid #00a7ea;\ padding:10px;\ background:#03c9fa;\ list-style-type:none;\ }\ .dom-abut-result li{\ padding:5px ;\ margin-bottom:1px;\ font-size:14px;\ }\ .dom-abut-slide{\ cursor: pointer;\ }\ .dom-abut-result li blockquote{\ margin:0;\ padding:5px;\ display:none;\ }\ .dom-abut-title{\ background:#008000;\ }\ .dom-abut-pass{\ background:#a9ea00;\ }\ .dom-abut-unpass{\ background:red;\ color:#fff;\ }\ .dom-abut-log{\ background:#c0c0c0;\ }\ .dom-abut-log blockquote{\ background:#808080;\ }"); } try { abut.isModify && eval(uneval); eval(resolving.join("")); } catch (e) { return abut.render("dom-abut-unpass","解析编译测试代码失败"); } for(var i=0,fn;fn= abut.closures[i++];){ try { fn(); } catch (e) { abut.render("dom-abut-unpass","第"+fn.lineNumber +"行测试代码执行失败"); } } } applyIf(dom,{ abut:function(obj){ var key = obj.selector || obj.url, target = obj.target || document.body,str; if(dom.type(target,"String")){ target = document.getElementById(target); } if(obj.selector){ var el = document.getElementById(key); if (!el) throw "can not find the target element"; str = el.text; }else { var xhr = dom.xhr(); xhr.open("GET",key+"?"+(new Date-0),false); xhr.send(null); str = xhr.responseText || ""; if (!str) throw "the target file does not exist"; } evalCode(str,target) }, getComments : function(text){ var m , result = []; while(m = rcomments.exec(text)){ result.push((m[1] || m[2]).replace(rstar,function($,$1){ return $1 })); } return result.join('\n'); }, addSheet : function(css){ if(!-[1,]){ css = css.replace(ropacity,function($,$1){ $1 = parseFloat($1) * 100; if($1 < 0 || $1 > 100) return ""; return "filter:alpha(opacity="+ $1 +");" }); } css += "\n";//增加末尾的换行符,方便在firebug下的查看。 var doc = document, head = doc.getElementsByTagName("head")[0], styles = head.getElementsByTagName("style"),style,media; if(!styles.length){//如果不存在style元素则创建 style = doc.createElement('style'); style.setAttribute("type", "text/css"); head.insertBefore(style,null) } style = styles[0]; media = style.getAttribute("media"); if(media === null && !/screen/i.test(media) ){ style.setAttribute("media","all"); } if(style.styleSheet){ //ie style.styleSheet.cssText += css;//添加新的内部样式 }else if(doc.getBoxObjectFor){ style.innerHTML += css;//火狐支持直接innerHTML添加样式表字串 }else{ style.appendChild(doc.createTextNode(css)) } }, //比较对象是否相等或相似 isEqual: function(a, b) { if (a === b) return true; var atype = typeof(a), btype = typeof(b); if (atype != btype) return false; if (a == b) return true; if ((!a && b) || (a && !b)) return false; if (a.isEqual) return a.isEqual(b); if (dom.type(a,"Date") && dom.type(b,"Date")) return a.valueOf() === b.valueOf(); if (dom.type(a,"NaN") && dom.type(b,"NaN")) return false; if (dom.type(a,"RegExp") && dom.type(b,"RegExp")) return a.source === b.source && a.global === b.global && a.ignoreCase === b.ignoreCase && a.multiline === b.multiline; if (atype !== 'object') return false; if (a.length && (a.length !== b.length)) return false; var aKeys = Object.keys(a), bKeys = Object.keys(b); if (aKeys.length != bKeys.length) return false; for (var key in a) if (!(key in b) || !dom.isEqual(a[key], b[key])) return false; return true; }, inspect : function(obj, indent) { indent = indent || ""; if (obj === null) return indent + "null"; if (obj === void 0) return indent + "undefined"; if (obj.nodeType === 9) return indent + "[object Document]"; if (obj.nodeType) return indent + "[object " + (obj.tagName || "Node") +"]"; var arr = [],type = dom.type(obj),self = arguments.callee,next = indent + "\t"; switch (type) { case "Boolean": case "Number": case "NaN": case "RegExp": return indent + obj; case "String": return indent + obj.quote(); case "Function": return (indent + obj).replace(/\n/g, "\n" + indent); case "Date": return indent + '(new Date(' + obj.valueOf() + '))'; case "Unknown": case "XMLHttpRequest" : case "Window" : return indent + "[object "+type +"]"; case "NodeList": case "HTMLCollection": case "Arguments": case "Array": for (var i = 0, n = obj.length; i < n; ++i) arr.push(self(obj[i], next).replace(/^\s* /g, next)); return indent + "[\n" + arr.join(",\n") + "\n" + indent + "]"; default: for (var i in obj) { arr.push(next + self(i) + ": " + self(obj[i], next).replace(/^\s+/g, "") ); } return indent + "{\n" + arr.join(",\n") + "\n" + indent + "}"; } } }); applyIf(dom.abut,{ //布尔测试 ok : function(state){ var bool = !!state, self = arguments.callee, lineNumber = self.lineNumber, testCode = self.testCode; this.prepareRender(bool,lineNumber,testCode); }, //同值性测试 eq : function(actual, expected){ var bool = actual == expected, self = arguments.callee, lineNumber = self.lineNumber, testCode = self.testCode; alert("lineNumber" + lineNumber) this.prepareRender(bool,lineNumber,testCode); }, //同一性测试 same : function(actual, expected){ var bool = dom.isEqual(actual, expected), self = arguments.callee, lineNumber = self.lineNumber, testCode = self.testCode; this.prepareRender(bool,lineNumber,testCode); }, log : function(obj, message){ var context = '"; var testCode = ""+dom.inspect(obj)+""; this.render("dom-abut-log",context,testCode); }, prepareRender : function(bool,lineNumber,testCode){ var className = bool ? 'dom-abut-pass' : 'dom-abut-unpass', context = ' " ; this.recoder.innerHTML = " 已完成第"+(++this.time)+"个测试"; this.render(className,context,testCode); }, render : function(className,context,code){ var li = document.createElement("li"); li.className = className; this.el.appendChild(li); var blockquote = document.createElement("blockquote") li.innerHTML = context; if(code){ li.appendChild(blockquote); blockquote.innerHTML = code; } } }); })();
测试例子1:
var test1 = 1; /* * <<<< * $$$$log(a) * >>>> */ /* dsfds */ // frgtretr /** * erwwer * */
测试例子2:
var p = function(){} // comment /* not-a-nested-comment p('not-a-comment'); // comment */* still-a-comment p('not-a-comment'); /* alert('commented-out-code'); // still-a-comment */ p('not-a-comment'); var re= /\/* not-a-comment */; //* comment
abut v2增加了对script的支持,为此方法的参数由一个字符串改为一个哈希,哈希有三个值,selector,url与target。selector就是指script标签的id,通过来获取其innerHTML进行测试。url指JS文件的路径,我们通过同步AJAX请求获取responseText进行测试,由于是AJAX请求,就会受到同源策略的限制,因此v1我就无法放出示例,因此以后示例都使用selector方法。target是指将测试结果放到哪一个元素节点中,默认是document.body,此参数可选,并且如果是字符串,它会自动当成标签的ID进行寻找。
比如我页面上有一个id为test3的script元素节点,其innerHTML如下:
var Person = function(name,sex){ this.name = name; this.sex = sex; //####log(this.name); //####log(this.sex); } var p = new Person("ruby","louvre");
想对其测试,只要引用abut.js,在页面上运行如下脚本即可:
dom.abut({ selector:id,//要测试的script标签 //url:url,//只限于同域的路径 target:document.body //把测试结果添加到的位置(可以是元素节点,也可以是其id值) });
在此例子中,我们也可以看到新的标识符####,它将取代原先的@@@@。v1中的@@@@目的是将闭包中或内部函数中的数据释出全局作用域下,然后再用$$$$一系列方法对它进行测试。然后,实践证明,这有点麻烦,为何不直接在原先的内部作用域中执行它呢?!####就是基于此目的开发出来,相对应,@@@@就没有用了,被我废弃掉了,移除了。####会对源码进行修改再进行解释执行,而$$$$则先解析执行源码再对注释中的测试样例进行修葺后执行,执行时机是完全不一样的。$$$$可以跟eq,same,ok,log这四个函数名,####也完全一样,但####暂时不支持写在<<<<与>>>>之间。
例子4,对多行测试注释的使用。
var flatten = function(arr) { var result = [],self = arguments.callee; for(var i=0,n=arr.length,el;i < n;i++){ el = arr[i]; if (dom.type(el,"Array")) { result = result.concat(self(el)); } else { result.push(el); } } return result; } /*<<<< * var a =['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]]; * $$$$same(flatten(a),['frank', 'bob', 'lisa', 'jill', 'tom', 'sally']); * >>>> */
例子5(截取自我框架的base模块)
var PROTO = "prototype", CTOR = "constructor", hasOwn = Object[PROTO].hasOwnProperty; //用于取得数据的类型或判定数据的类型 // $$$$(dom.type(1,"Number")); // $$$$(dom.type(NaN,"NaN")); // $$$$(dom.type(void(0),"Undefined")); // $$$$(dom.type("aaa","String")); // $$$$(dom.type([1,2,3],"Array")); // $$$$(dom.type(/i/,"RegExp")); // $$$$(dom.type({},"Object")); // $$$$(dom.type(document,"Document")); // $$$$(dom.type(document.body,"BODY")); // $$$$(dom.type(window,"Window")); // $$$$(dom.type(true,"Boolean")); // $$$$(dom.type(document.getElementsByTagName('script'),"HTMLCollection")); dom.type = (function(){ var reg = /^(\w)/, regFn = function($,$1){ return $1.toUpperCase() }, to_s = Object[PROTO].toString; return function(obj,str){ var result = (typeof obj).replace(reg,regFn); if(result === 'Object' || (result === 'Function' && obj.exec) ){//safari chrome中 type /i/ 为function if(obj===null) result = 'Null'; else if(obj.window==obj) result = 'Window'; //返回Window的构造器名字 else if(obj.callee) result = 'Arguments'; else if(obj.nodeType === 9) result = 'Document'; else if(obj.nodeName) result = (obj.nodeName+'').replace('#',''); //处理元素节点 else if(!obj.constructor || !(obj instanceof Object)){ if("send" in obj && "setRequestHeader" in obj){//处理IE5-8的宿主对象与节点集合 result = "XMLHttpRequest" }else if("length" in obj && "item" in obj){ result = "namedItem" in obj ? 'HTMLCollection' :'NodeList'; }else{ result = 'Unknown'; } }else result = to_s.call(obj).slice(8,-1); } if(result === "Number" && isNaN(obj)) result = "NaN"; //safari chrome中 对 HTMLCollection与NodeList的to_s都为 "NodeList",此bug暂时无解 if(str){ return str === result; } return result; } })() //生成键值统一的对象,用于高速化判定 // $$$$same(dom.oneObject(["aa","bb","cc"]),{"aa":1,"bb":1,"cc":1}) dom.oneObject = function(array,val){ var result = {},value = val !== void 0 ? val :1; for(var i=0,n=array.length;i < n;i++) result[array[i]] = value; return result; }, // $$$$eq(dom.isPlainObject({}),true) // $$$$eq(dom.isPlainObject({aa:"aa",bb:"bb",cc:"cc"}),true) // $$$$eq(dom.isPlainObject(new Object),true) // $$$$eq(dom.isPlainObject(window),false) // $$$$eq(dom.isPlainObject(document.body),false) dom.isPlainObject = function (obj){ return !!obj && dom.type(obj,"Object") && hasOwn.call(obj[CTOR][PROTO],"isPrototypeOf") ; }