Ruby's Louvre

每天学习一点点算法

导航

javascript 测试工具 abut v2

综合众人的意见,此版本做了许多改进,如对注释抽取的优化,增加对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 = '第' + arguments.callee.lineNumber+"行日志记录  "+ (message || "") + "";
            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 = '第'+ lineNumber+'行测试代码: '+(bool ? '通过' :'不通过' )+"" ; 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") ;
      }

posted on 2010-11-08 09:13  司徒正美  阅读(3264)  评论(7编辑  收藏  举报