js实现按键精灵——尝试前端实现自动化测试(一)

作者:寒草
微信:hancao97
介绍:一个不一样的北漂程序员(工作10个月的年轻程序员),欢迎加微信批评指正交流技术或者一起玩耍约饭,或者相亲呜呜呜,520被虐惨了

背景

为什么要做这件事?

最近我们在新项目的联调接测阶段遇到了很大的问题,具体的问题我也不在这里过多的描述了,反正最终造成了前端同学们心力憔悴,怨声载道,哈哈哈哈(我这里也不是在抱怨谁谁谁,但是遇到问题我们就要去想怎么样去解决问题~)。于是我就在想如何让我们在这些阶段获得更加舒适的体验。
经过一点点的思考,我打算提供一些自动化的能力,可以让我们自动化过接测用例,并在过接测用例的同时,同时监测接口的信息,在过完接测用例后把有问题的接口调用呈现给我们的开发者。

github:github.com/CodingCommu…

大家别误会,其实并没有开发完成,我也只是在茶余饭后,闲来无事的时候进行了一部分的技术调研,在这里和大家介绍一下我的想法。

怎么做这件事?

在说我的想法之前,我想说一下,我想要去做的事:

  • 提供自动化过接测用例/自动化自测的能力
  • 运行过程中接口监测能力

ok,既然明白了我的需求,那就可以去想一下如何去实现了。
image.png
在最开始的时候,我做了这样的一个图,图中有上到下分为四层,我逐层为大家介绍:

  • 第一层:我阐述了我们要做的功能,主要是做前端的轻量自动化测试,包括接测自测自动化接口测试
  • 第二层:第二层其实我阐述我把这个需求分成了两部分,一部分是自动化能力,一部分是接口监测能力。当然本文主要介绍的是自动化能力。
  • 第三层:我对第二层中的自动化能力接口监测能力做了拆解(ps:我后面会详细介绍自动化能力部分,接口监测埋个坑吧,下次再说~)
  • 第四层:我希望我这个自动化测试的工具采用的呈现方式为Chrome插件,因为我认为,chrome插件我们使用起来更加方便,而且我可以利用到他提供的webRequest对接口调用,流量信息的观察分析能力(此处待调研)。

自动化能力拆解

首先我对我要做的事情第一感觉就是按键精灵
我希望的是我们可以只操作一遍,便可以对这个操作流程完成录制,之后只需要自动的进行这些操作就好了。ok,既然我们想要的是按键精灵之类的东西,那我就这个需求进行拆解:

  1. 录制(本文重点)

我们应该都可以预想到,我们第一步要做的就是对用户的操作流程进行记录.但是我们这个阶段要思考的问题是如何记录记录哪些信息记录哪些操作怎么记录才能易于自动化复现

  1. 操作流程->中间数据

我们录制的最终目的是要下一次需要自测或者过接测用例的时候进行复现的,所以其实,我们肯定是要对录制的信息转换为某种数据结构,并保存在本地

  1. 中间数据->js脚本

可以将中间数据转换成js脚本,这个理解就比较容易,就是把之前本地存储的中间数据转换为js脚本

  1. 自执行脚本

js脚本自动执行,重现用户操作。这也就是重现用户操作的最后一步。

ok,自动化能力简单的拆解完成之后,我们就开始本文的主要内容,如何录制用户操作?

用户操作录制

把用户操作记录下来其实不算难,但是要考虑我们需要记录哪些信息以方便我们在后续操作中对用户操作记录进行重现。
所以我在这里不仅会介绍我对记录用户操作的想法,也会去介绍我将要如何利用我记录的信息对用户操作进行重现
我这里主要介绍的用户操作包括三种:

  • 点击操作
  • 键盘输入
  • 屏幕滚动

看似只有三个,但是已经覆盖了我们业务中的比较多的场景。
github:github.com/CodingCommu…
可以查看webApi.html包括本博客所有代码~

tip: 所有点击事件,滚动事件,键盘输入事件都需要记录时间点,相对最开始时间偏移,以正确重现.本文后续就不介绍记录时间相关的内容了

鼠标点击

代码

dom:

<button class="btn">
    nihao
</button>
复制代码

js:

document.onclick = function (ev) {
    const x = ev.clientX;
    const y = ev.clientY;
    setTimeout(() => {
        document.elementFromPoint(x, y).dispatchEvent(clickEvent);
    }, 2000)
}
document.getElementsByClassName('btn')[0].onclick = function () {
    console.log(11, 1)
}
复制代码

讲解

首先,记录用户的点击事件并不是一件难事,但是我想要的结果是在将前文中的中间数据->js脚本环节更加方便,所以我有这样的一个原则:

不使用记录class,id等标识再去通过其查找dom元素的方式进行复现操作。
期望直接通过坐标点寻找元素

所以向大家介绍一个Api,大家可能没有用过,但是在我们这个场景下特别好用:
document.elementFromPoint(x, y)
大家看我的代码,我给document绑定点击事件,记录下clientXclientY
简单介绍一下clientX:
image.png
这里我使用一下别的文章中对于clientX的介绍。

MDN:MouseEvent.clientX 是只读属性, 它提供事件发生时的应用客户端区域的水平坐标 (与页面坐标不同)。例如,不论页面是否有水平滚动,当你点击客户端区域的左上角时,鼠标事件的 clientX 值都将为 0 。

我们记录了点击的位置之后我们在复线操作阶段,只需要通过document.elementFromPoint(x, y)获取元素之后再为它dispatchEvent点击事件就ok了。

相关知识:

mouseEvent:developer.mozilla.org/zh-CN/docs/…
dispatchEvent: developer.mozilla.org/zh-CN/docs/…

更多测试:

<!-用于测试多种类型的点击事件可否派发-->
<label><input name="Fruit" type="radio" value="" class="aaa" />苹果 </label>
<label><input name="Fruit" type="radio" value="" class="bbb" />桃子 </label>
<label><input name="Fruit" type="radio" value="" />香蕉 </label>
<label><input name="Fruit" type="radio" value="" /></label>
<label><input name="Fruit" type="radio" value="" />其它 </label>
复制代码
// 测试radio的点击事件是否可以派发
document.getElementsByClassName('bbb')[0].dispatchEvent(clickEvent)
document.getElementsByClassName('aaa')[0].dispatchEvent(focusEvent)
复制代码

键盘输入

代码

// 测试键盘输入事件
// 做法:假定,持续的键盘输入都是对同一个 input的输入,
// 那我需要做的就是保存输入顺序,并记录上一刻点击的元素,并改变其value!!判断是不是input(有没有value属性来判断),如果不是用innerHtml/innerText去塞!!!为了支持富文本
document.onkeydown = function (ev) {
    console.log(ev)
}
const keyTestEl = document.getElementsByClassName('inputTest')[0];
keyTestEl.value = '11111'
复制代码

讲解

其实键盘输入记录我的想法就很简单,我上面代码中的测试内容已经可以描述我的想法了。
用户操作流程:
image.png
用户会点击某个元素,之后会进行持续输入,知道下一个点击事件触发则表示用户输入结束,就是对该dom元素的键盘操作事件结束。
复现键盘输入事件:

image.png
其实就是按照流程图的思路来,就可以完成建议的键盘事件的录制与复现。
借助的只是:document.onkeydown和按照元素类型修改value或者innerHtml属性。当然也需要借助前文的document.elementFromPoint(x, y)获取并缓存点击的元素。
于是点击事件到此为止了~

页面滚动(场景比较复杂)

代码

dom:

<div class="scroll-container">
    <div class="scroll-content">
        <div class="scroll-inner">
            内部滚动测试
        </div>
    </div>
</div>
复制代码

css:

body {
    height: 1800px;
}

.scroll-container {
    max-height: 500px;
    background: pink;
    overflow: auto;
}

.scroll-content {
    height: 800px;

}

.scroll-inner {
    width: 300px;
    height: 300px;
    background-color: purple;
}
复制代码

js:

let mouseX = 0;
let mouseY = 0;
let scrollStartEl = null; //用于记录滚动的起始元素,为了保证重现操作时为元素设置scrollTop时不出现偏差
let scrollRecordList = [];
let scrollElementSet = new Set();

// 通用节流方法
const throttle = function (cb, delay = 100) {
    let timer = null;
    return (ev) => {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            cb && cb(ev);
        }, delay)
    };
}

// 绑定滚动事件
setScrollWatcher = function (ev) {
    mouseX = ev && ev.clientX || mouseX;
    mouseY = ev && ev.clientY || mouseY;
    scrollStartEl = document.elementFromPoint(mouseX, mouseY);
    let el = scrollStartEl;
    while (el) {
        if (scrollElementSet.has(el)) {
            el = null;
        } else {
            el.onscroll = throttle(recordScrollInfo);
            scrollElementSet.add(el);
            el = el.parentNode;
        }
    }
};

// 记录滚动信息
recordScrollInfo = function (ev) {
    let el = scrollStartEl;
    // 单纯的滚动也可能引起鼠标对应的dom的变化,滚动结束也需要setScrollWatcher
    setScrollWatcher();
    let scrollRecordInfo = {
        mouseX: mouseX,
        mouseY: mouseY,
        scrollTopList: []
    }
    while (el) {
        scrollRecordInfo.scrollTopList.push(el.scrollTop);
        el = el.parentNode;
    }
    scrollRecordList.push(scrollRecordInfo);
    console.log(scrollRecordList)
}

// 绑定鼠标移动事件
document.onmousemove = throttle(setScrollWatcher);
复制代码

讲解

这里大家请跟着我的思路一步一步来:

  1. 首先我给document绑定了鼠标移动事件,并且为其设置了节流
// 绑定鼠标移动事件
document.onmousemove = throttle(setScrollWatcher);
复制代码

我现在得到了鼠标最后停止的位置,我的思路是,如果现在用户开始滚动鼠标滚轮,那么页面可能发生的滚动可能就出现在,鼠标悬停位置对应的元素及其所有祖先元素上!

  1. 正如前面的介绍,我现在要做的就是给鼠标悬停位置对应的元素及其所有祖先元素绑定滚动事件,即setScrollWatcher方法所做的事情。
let mouseX = 0;
let mouseY = 0;
let scrollStartEl = null; //用于记录滚动的起始元素,为了保证重现操作时为元素设置scrollTop时不出现偏差
let scrollElementSet = new Set();

// 绑定滚动事件
setScrollWatcher = function (ev) {
    mouseX = ev && ev.clientX || mouseX;
    mouseY = ev && ev.clientY || mouseY;
    scrollStartEl = document.elementFromPoint(mouseX, mouseY);
    let el = scrollStartEl;
    while (el) {
        if (scrollElementSet.has(el)) {
            el = null;
        } else {
            el.onscroll = throttle(recordScrollInfo);
            scrollElementSet.add(el);
            el = el.parentNode;
        }
    }
};
复制代码

大家看的应该也不是很费劲,首先鼠标停下来了,我就记录一下现在鼠标所在的元素存在scrollStartEl中,之后为它以及他的所有祖先元素设置滚动事件(当然滚动事件也做了节流处理~)。

优化:其中为了防止重复绑定滚动事件,也做了一定的优化,引入了一个scrollElementSet,如果set中存在这个元素,那么直接结束循环。(tip:因为一个元素只会有一个直接父元素

  1. 下一步就显而易见了,就是要在滚动事件结束之后对其进行记录~
recordScrollInfo = function (ev) {
    let el = scrollStartEl;
    // 单纯的滚动也可能引起鼠标对应的dom的变化,滚动结束也需要setScrollWatcher
    setScrollWatcher();
    let scrollRecordInfo = {
        mouseX: mouseX,
        mouseY: mouseY,
        scrollTopList: []
    }
    while (el) {
        scrollRecordInfo.scrollTopList.push(el.scrollTop);
        el = el.parentNode;
    }
    scrollRecordList.push(scrollRecordInfo);
    console.log(scrollRecordList)
}
复制代码

我只需要记录滚动开始鼠标所在元素scrollStartEl及其所有祖先元素的scrollTop就可以了,之后在复线操作的过程中,根据记录的mouseXmouseY找到滚动开始元素,并为其以及其所有祖先元素设置scrollTop即可~

注意

  1. 单纯的滚动也可能引起鼠标对应的dom的变化,滚动结束也需要setScrollWatcher
  2. 因为滚动结束后鼠标位置对应的元素可能会变,所以确实需要在滚动前记录滚动开始元素,防止复现操作时出现元素层级不一致问题。

我感觉这个滚动的记录还是有点骚的,哈哈哈,我就是这么臭不要脸,哈哈哈~

完整代码【方便查看】

完整的测试代码在此,如果上github不方便,可以直接复制粘贴试一下~

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style type="text/css">
        body {
            height: 1800px;
        }

        .scroll-container {
            max-height: 500px;
            background: pink;
            overflow: auto;
        }

        .scroll-content {
            height: 800px;

        }

        .scroll-inner {
            width: 300px;
            height: 300px;
            background-color: purple;
        }

        .btn {
            margin-top: 1000px;
        }
    </style>
</head>

<body>
    <!-用于测试多种类型的点击事件可否派发-->
    <label><input name="Fruit" type="radio" value="" class="aaa" />苹果 </label>
    <label><input name="Fruit" type="radio" value="" class="bbb" />桃子 </label>
    <label><input name="Fruit" type="radio" value="" />香蕉 </label>
    <label><input name="Fruit" type="radio" value="" /></label>
    <label><input name="Fruit" type="radio" value="" />其它 </label>
    <!-用于测试keydown事件-->
    <input class ="inputTest" />
    <button class="btn">
        nihao
    </button>
    <div class="scroll-container">
        <div class="scroll-content">
            <div class="scroll-inner">
                内部滚动测试
            </div>
        </div>
    </div>
    <script>
        /*
        * 第一步:完成点击事件,滚动事件,键盘输入事件的记录和重现。
        * TODO:待办:拖拽事件?
        */
        const clickEvent = new MouseEvent('click');
        const focusEvent = new FocusEvent('focus',{
            view: window
        });

        // 通用节流方法
        const throttle = function (cb, delay = 100) {
            let timer = null;
            return (ev) => {
                if (timer) {
                    clearTimeout(timer)
                }
                timer = setTimeout(() => {
                    cb && cb(ev);
                }, delay)
            };
        }
        //TODO: 所有点击事件,滚动事件,键盘输入事件都需要记录时间点,相对最开始时间偏移,以正确重现
        // 测试点击事件
        document.onclick = function (ev) {
            const x = ev.clientX;
            const y = ev.clientY;
            setTimeout(() => {
                document.elementFromPoint(x, y).dispatchEvent(clickEvent);
            }, 2000)
        }
        document.getElementsByClassName('btn')[0].onclick = function () {
            console.log(11, 1)
        }

        // TODO:(待验证)测试重现滚动事件
        let mouseX = 0;
        let mouseY = 0;
        let scrollStartEl = null; //用于记录滚动的起始元素,为了保证重现操作时为元素设置scrollTop时不出现偏差
        let scrollRecordList = [];
        let scrollElementSet = new Set();
        setScrollWatcher = function (ev) {
            mouseX = ev && ev.clientX || mouseX;
            mouseY = ev && ev.clientY || mouseY;
            scrollStartEl = document.elementFromPoint(mouseX, mouseY);
            let el = scrollStartEl;
            while (el) {
                if (scrollElementSet.has(el)) {
                    el = null;
                } else {
                    el.onscroll = throttle(recordScrollInfo);
                    scrollElementSet.add(el);
                    el = el.parentNode;
                }
            }
        };
        recordScrollInfo = function (ev) {
            let el = scrollStartEl;
            // 单纯的滚动也可能引起鼠标对应的dom的变化,滚动结束也需要setScrollWatcher
            setScrollWatcher();
            let scrollRecordInfo = {
                mouseX: mouseX,
                mouseY: mouseY,
                scrollTopList: []
            }
            while (el) {
                scrollRecordInfo.scrollTopList.push(el.scrollTop);
                el = el.parentNode;
            }
            scrollRecordList.push(scrollRecordInfo);
            console.log(scrollRecordList)
        }
        // 绑定鼠标移动事件
        document.onmousemove = throttle(setScrollWatcher);

        // 测试键盘输入事件
        // 做法:假定,持续的键盘输入都是对同一个 input的输入,
        // 那我需要做的就是保存输入顺序,并记录上一刻点击的元素,并改变其value!!判断是不是input(有没有value属性来判断),如果不是用innerHtml/innerText去塞!!!为了支持富文本
        document.onkeydown = function (ev) {
            console.log(ev)
        }
        const keyTestEl = document.getElementsByClassName('inputTest')[0];
        keyTestEl.dispatchEvent(clickEvent)
        console.log(keyTestEl, focusEvent)
        keyTestEl.dispatchEvent(focusEvent)
        // const keyEvent = new KeyboardEvent('keypress',{'key':'a'})
        //keyTestEl.dispatchEvent(keyEvent)
        //keyTestEl.value = '11111'

        // 测试radio的点击事件是否可以派发
        document.getElementsByClassName('bbb')[0].dispatchEvent(clickEvent)
        document.getElementsByClassName('aaa')[0].dispatchEvent(focusEvent)
    </script>
</body>

</html>
复制代码

结束语

image.png
眼里要有小星星,生活才能亮晶晶~
感谢大家阅读,都要快乐生活哦~
下一篇文章,故事继续哈!

最后的最后:感兴趣也可以看看我之前的文章哈~ 也可以加我微信哈!
微信:hancao97

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享