A-A+

海豚中用户交互行为录制功能的实现详解

2017年07月31日 自动化测试 暂无评论 阅读 172 次

之前有同学(@jet )对海豚录制这块的功能感兴趣,所以就专门写一个帖子来说一下海豚录制功能的实现逻辑以及解决了哪些坑。

下面详细说一下海豚中录制功能的实现细节以及代码DEMO。

海豚对录制功能做了比较多的考量,前期也在这方面栽了很多跟头,比如:

  1. 获取到的DOM元素的Selector,选择出来之后,可能页面会有多个DOM元素的情况,因为有些ID相同的容器,只是隐藏了而已
  2. 点击事件的时候,DOM元素绑定了有mousedown/mouseup事件去实现触摸反馈的样式,这样className是变化了的,导致获取到的Selector是错误的,回放的时候找不到DOM元素
  3. 一些元素的ID是时间戳或者随机数字的形式,每次页面加载都不一样,比如:id-123123 这样的形式,数字是每次都会变化的,导致录制获取到的Selector是某个时刻的,回放又是一个问题。
  4. 录制的Selector太长了,可读性太差,导致无法很好直观的看到点击的是什么内容
  5. ...

接下来看看海豚中的录制工具针对上面的N多录制相关的问题的解决方式(以点击事件为例子):

海豚的录制功能是全JS实现的,有一个前提就是,页面的点击事件的捕获和冒泡机制没有被破坏

第一步:监控页面的点击事件

document.addEventListener("click",function(e){
    if(isNotRecording){ return; }

    var target = e.target,
        tagName =target.tagName && target.tagName.toLowerCase() || '';

    //如果是HTML控件的话,则不记录click事件
    if(isIgnoreElement(target)){ return; }

    if(tagName === "html" || tagName === "body"){ return; }

    actions.push("click::" + window.getSelector(target) + "::" + (+new Date()));
    actionPaths.push({
        url : window.location.href,
        title : target.title,
        id : target.getAttribute('_id_'),
        className : target.getAttribute('_class_'),
        selector : window.getSelector(target),
        tagName : target.tagName.toLowerCase(),
        innerText : target.innerText || target.value,
        event : 'click',
        target : target
    });
}, true);

上面有一个isIgnoreElement函数的调用,主要是为了屏蔽掉一些元素的点击事件,比如一些HTML控件:

var isIgnoreElement = function(target){
    if(!target){ return false; }

    return ' textarea select option '.indexOf(' ' + target.tagName.toLowerCase() + ' ') !== -1;
}

window.getSelector方法就是获取点击目标DOM元素的Selector,这个扩展开来讲一下获取Selector的大概实现逻辑。

首先为了解决一些模拟触摸反馈导致className变化的场景,那么就需要把页面渲染之后全部DOM元素的id和class都储存起来,然后获取的时候都获取这些储存的id和class:

var tagIdAndClass = function(element){
    if(element && element.id){
        element.setAttribute('_id_', element.id);
    }

    if(element && element.className){
        element.setAttribute('_class_', element.className);
    }

    var childNodes = element.childNodes;
    if(childNodes.length){
        for(var i = 0,len = childNodes.length; i < len; i++){
            tagIdAndClass(childNodes[i]);
        }
    }
}

tagIdAndClass(document.body);
//DOM结构有变化的时候,重新将新增的元素的id和class储存起来
document.addEventListener('DOMNodeInserted', function(e){
    var elem = e.target;

    tagIdAndClass(elem);
});

然后,点击该元素之后,就是获取Selector的逻辑:

var _getSelector_ = function(element){
    if(!element){ return; }

    if(typeof element === 'string'){ return element; }

    var tagName = element.tagName && element.tagName.toLowerCase() || '';

    if(!tagName){ return ''; }

    function trim(string){
        return string && string.toString().replace(/^s+|s+$/,"") || string;
    }

    //去掉一些使用时间戳作为ID的元素
    var id = element.getAttribute('_id_');
    if(id && !(/d{3,13}/).test(id) && !(/^d+$/).test(id)){
        //如果这个ID在页面上是唯一的,那么就返回该ID,否则再继续往上层父元素添加selector
        if(document.querySelectorAll('#' + id).length === 1){
            return '#' + id;
        }
    }

    if(element == document || element == document.documentElement){
        return 'html';
    }

    if (element == document.body){ return 'html > ' + element.tagName.toLowerCase(); }


    if(!element.parentNode){return element.tagName.toLowerCase();}

    var ix = 0, 
        siblings = element.parentNode.childNodes,
        elementTagLength = 0,
        classname = trim(element.getAttribute('_class_'));

    //判断该className是否在整个文档中是唯一的
    if(classname && document.querySelectorAll("." + classname.replace(/s+/g,".")).length === 1){
        return "." + classname.replace(/s+/g,".");
    }

    for (var i = 0,l = siblings.length; i < l; i++) {
        if(classname){
            if(siblings[i].nodeType === 1 && (trim(siblings[i].getAttribute('_class_')) === classname)){
                ++elementTagLength;
            }
        }else{
            if((siblings[i].nodeType == 1) && (siblings[i].tagName === element.tagName)){
                ++elementTagLength;
            }
        }
    }

    for (var i = 0,l = siblings.length; i < l; i++) {
        var sibling = siblings[i];
        if (sibling === element){
            return arguments.callee(element.parentNode) + ' > ' + (classname ? "." + classname.replace(/s+/g,".") : element.tagName.toLowerCase()) + ((!ix && elementTagLength === 1) ? '' : ':nth-child(' + (ix + 1) + ')');
        }else if(sibling.nodeType == 1){
            ix++;
        }
    }
};

通过上面的方式,解决了id带有随机数字的场景。但是获取到的Selector可能比较长,那么需要缩短一下:

window.getSelector = function(element){
    var selector = _getSelector_(element);

    var element = document.querySelector(selector),
        selectors = selector.split('>'),
        length = selectors.length,
        preSelector = selector;

    for(var i = 1; i < length; i++){
        var css = selectors.slice(i, length).join('>');
        if(document.querySelector(css) !== element){
            break;
        }
        preSelector = css;
    }

    return preSelector.trim();
}

上面获取到的Selector就是最终所需要的Selector了,可以用在回放的逻辑里面。但是还有一个问题,该Selector最终不能唯一定位一个元素,那么就需要去重,这个去重的逻辑就是在回放的逻辑里面,只获取到显示出来那一个DOM元素:

var visible = function(elem){
    $elem = Zepto(elem);
    return !!($elem.width() || $elem.height()) && $elem.css("display") !== "none"
}

var getTarget = function(element){
    var targets = document.querySelectorAll(element),
        target;
    //如果有多个,则获取到visible的那一个
    if(targets.length > 1){
        for(var i = 0, len = targets.length; i < len; i++){
            if(visible(targets[i])){
                target = targets[i];
                break;
            }
        }
    } else {
        target = targets[0];
    }

    return target;
}

至此,监控页面点击事件,以及获取DOM元素的Selector逻辑完毕。

第二步:将收集的点击行为自动转成可直接运行的测试代码:

这里海豚定义了action相关的api(以点击事件为例):

var action = monitor.createAction();
action
.wait(2000)
.click("#content > div > div:nth-child(3) > .indexInner", {  
    waitTime : 2000,
}, function(elem){
    //elem参数为当前Selector的DOM引用
    monitor.log(window.location.href);
})
.end(function(){ monitor.complete(); });

上面就是一个录制后自动生成的一段可直接执行的测试代码,对用户来说可以不用改变什么,就可以执行这个交互行为,并配合 page-diff 逻辑,就可以监控交互行为后页面的UI变化。

var action = monitor.createAction();
action
.wait(2000)
.click("#content > div > div:nth-child(3) > .indexInner", {  
    usePageDiff : true, 
    waitTime : 2000,
    root : '#content',
    excludeSelectors : ['.tips', '.ads'],
    ignoreTextSelectors : true
}, function(elem){
    //elem参数为当前Selector的DOM引用
    monitor.log(window.location.href);
})
.end(function(){ monitor.complete(); });

这里对waitTime说明一下,它的作用是执行了该点击行为之后,多久去进行pagediff操作,避免一些异步操作导致加载缓慢使得diff出错的问题。

好了,海豚整个录制功能就是这个样子的了。

https://testerhome.com/topics/4259

标签:

给我留言

Copyright © web前端技术开发个人博客 保留所有权利  京ICP备14060653号 Theme  Ality

用户登录