最近在研究[Web自动化测试](https://www.cnblogs.com/strick/p/16892143.html),之前做了些实践,但效果并不理想。
  对于 QA 来说,公司的网页交互并不多,用手点点也能满足。对于前端来说,如果要做成自动化,就得维护一堆的脚本。
  当然,这些脚本也可以 QA 来维护,但前提是得让他们觉得做这件事的 ROI 很高,依目前的情况看,好像不高。
  所以在想,做一个平台,在这个平台中可以保存些数据,并且在旁边提供个小窗口,呈现要测试的 H5 网页,如下图所示(画图工具是[excalidraw](https://excalidraw.com/))。
  在修改相关数据后,可以直接看到网页的变化。
:-: ![](https://img.kancloud.cn/8b/23/8b235013653afbe5ed7b39d4bfffb12c_1101x938.png =400x)
  QA 或前端可以不用再写脚本代码,就能实现自动化测试。
  目前想到两块,第一块是拦截请求,mock 响应;第二块是记录页面行为,然后自动回放,最后截图,和上一次的截图做对比分析,看是否相同。
## 一、拦截请求
  拦截请求就是将响应 mock 成自己想要的数据,然后查看页面的呈现。
  这样就能模拟各种场景,毕竟测试环境的业务数据肯定不能满足所有场景,所以需要自己造。
  有了平台后,就能将造的数据保存在数据库中,可随时调取查看页面呈现。
**1)拦截**
  现在就要实现拦截,我首先想到的就是注入脚本,然后在 XMLHttpRequest 或 fetch() 埋入拦截代码。
  以 XMLHttpRequest 为例,在 monitorXHR() 函数中就可以让请求转发到代理处。
~~~
var _XMLHttpRequest = window.XMLHttpRequest; // 保存原生的XMLHttpRequest
// 覆盖XMLHttpRequest
window.XMLHttpRequest = function (flags) {
var req = new _XMLHttpRequest(flags); // 调用原生的XMLHttpRequest
monitorXHR(req); // 埋入我们的“间谍”
return req;
};
~~~
  例如将所有的请求都 post 到 test/proxy 接口,这是一个 Node 接口,代码如下。
  代码比较简单,没有考虑各种请求,例如自定义的 header、cookie 等。因为没有经过实践,只是展示下思路,所以肯定存在着 BUG。
  思路就是将整理好的请求地址、参数等信息转发过来后,先从数据库中查看是否有指定的 mock 数据。
  如果有就直接返回,若没有,就再去请求原接口。
~~~
router.post("/test/proxy", async (ctx) => {
const { id, method, url, params } = ctx.request.body;
// 通过ID查找存储在 MongoDB 中的拦截记录
const row = await services.app.getOne(id);
if (row) {
ctx.body = row.response;
return;
}
// 没有拦截就请求原接口
const { data } = await axios[method](url, params);
ctx.body = data;
});
~~~
  理论上,是完成了拦截,但是现在还有个很重要的问题,那就是 XMLHttpRequest 或 fetch() 那段间谍脚本该怎么注入。
**2)注入脚本**
  暂时想到了三个方法,第一个是通过控制 iframe 在页面中注入脚本。
  因为那张 H5 示例页面,可以放到 iframe 中呈现,所以这种注入方式理论上可行。
  只需要读取 HTMLIFrameElement 中的[contentDocument](https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/contentDocument)属性就能得到页面中的 document。
~~~
document.getElementById('inner').contentDocument.body.innerHTML
~~~
  但是 iframe 有个同源限制,必须是同源的才能通过脚本读取到 contentDocument。
  况且注入的时机也比较讲究,必须在发起请求之前,改写 XMLHttpRequest 或 fetch(),若用 JavaScript 添加 script 元素,恐怕不够及时。
  那么第二个方法,就是在构建的时候将脚本注入,当然,在上线后,这些脚本都是要去除掉的,仅限测试的时候使用。
  不过这种方法不够自动化,需要研发配合,像我们这种小公司,就那么几个项目,倒也问题不大。
  第三个方法是用无头浏览器(例如[puppeteer](https://pptr.dev/api/))将脚本注入(如下所示),然后再把新的页面结构作为响应返回。
~~~
await page.evaluate(async () => {
const img = new Image();
img.src = "xxx.png";
document.body.appendChild(img);
});
// 获取 HTML 结构
const html = await page.content();
~~~
  但有个地方要注意,输出页面结构的域名要和之前相同(需要运维配合),否则那些脚本很有可能因为跨域而无法执行了。
## 二、记录页面行为
  网页就是一棵 DOM 树,要记录页面行为,其实就是记录发生动作的 DOM 元素以及相关的动作参数。
  脚本注入的方式可以参考上面的 3 种方法,平台的布局也与上面的类似,只是表单中的参数可能略有不同。
**1)保存 DOM 元素**
  DOM 元素是不能直接 JSON 序列化的,所以需要将其映射成一个指定结构的对象,如下所示。
~~~
{
"type": "scrollTo",
"rect": {
"top": 470,
"left": 8,
"width": 359,
"height": 400
},
"scroll": {
"top": 189.5,
"left": 0
},
"tag": "div"
}
~~~
  tag 是元素类型,例如 div、button、window 等;type 是事件类型,例如点击、滚动等;rect 是坐标和尺寸,scroll 是滚动距离。
  这种结构就可以顺利存储到数据库中了。
**2)监控行为**
  目前实验,就只监控了点击和滚动两种行为。
  为 body 元素绑定 click 事件,采用捕获的事件传播方式。
~~~
/**
* 监控 body 内的点击行为
*/
document.body.addEventListener('click', (e) => {
behaviors.push({
type: 'click',
rect: offsetRect(e.target),
tag: e.target.tagName.toLowerCase()
});
}, true);
~~~
  rect 的尺寸和坐标本来是通过[getBoundingClientRect()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect)获取的,但是该方法参照的是视口的左上角,也就是说会随着滚动而改变坐标。
:-: ![](https://img.kancloud.cn/08/f7/08f7f10f7fcf5ad72fe28ff7ed1068b3_1466x1099.png =500x)
  所以就换了一种能更加精确获取坐标的方法,如下所示,nodeMap 是一个 Map 数据结构,key 可以是一个元素对象,用于缓存计算过的元素坐标。
~~~
// 元素缓存
const nodeMap = new Map();
/**
* 读取元素真实的坐标
*/
function offsetRect(node) {
// 从缓存中读取node信息
const exist = nodeMap.get(node);
if(exist) {
return exist;
}
let top = 0, left = 0;
const width = node.offsetWidth
const height = node.offsetHeight;
while (node) {
top += node.offsetTop;
left += node.offsetLeft;
node = node.offsetParent;
}
const rect = { top, left, width, height };
nodeMap.set(node, rect); // 缓存node信息
return rect;
}
~~~
  下面是对滚动的监控代码,throttle() 是一个节流函数,不节流会影响滚动的性能。
  在 startScroll() 函数中会计算滚动条距离顶部和左边的距离,window 和元素读取的属性略有不同。
~~~
/**
* 节流
*/
function throttle(fn, wait) {
let start = 0;
return (e) => {
const now = +new Date();
if (now - start > wait) {
fn(e);
start = now;
}
};
}
/**
* 对滚动节流
*/
const startScroll = throttle((e) => {
const target = e.target;
let tag, rect, scroll;
if(target.defaultView === window) {
tag = 'window';
scroll = {
top: window.pageYOffset,
left: window.pageXOffset
};
}else {
tag = target.tagName.toLowerCase();
scroll = {
top: target.scrollTop,
left: target.scrollLeft
};
rect = offsetRect(target);
}
behaviors.push({
type: 'scrollTo',
rect,
scroll,
tag
});
}, 100);
/**
* 监控页面的滚动行为
*/
window.addEventListener('scroll', (e) => {
startScroll(e);
}, true);
~~~
**3)还原**
  在得到数据结构后,就得让其还原,呈现完成一系列动作后的页面。
  我写的算法比较简单,还有很大的优化空间。目前就是遍历存储的行为数组,然后深度优先搜索 body 内的所有子元素。
  当坐标和尺寸满足条件时,返回元素。不过这种方式非常依赖这两个参数,因此只要结构发生变化,那么动作就无法完成。
~~~
function revert(behaviors) {
let isFind = false;
// 深度优先遍历
const dfs = (node, target) => {
if (!node) return;
const rect = offsetRect(node);
const tag = node.tagName.toLowerCase();
// console.log(node, rect, target)
// 根据坐标定位元素
if (target.tag === tag &&
target.rect.top === rect.top &&
target.rect.left === rect.left &&
target.rect.width === rect.width &&
target.rect.height === rect.height) {
target.node = node; //记录元素
isFind = true;
return;
}
node.children && Array.from(node.children).forEach((value) => {
if (isFind) { return; }
dfs(value, target);
});
};
behaviors.forEach(item => {
isFind = false;
// window对象单独处理
if(item.tag === 'window') {
item.node = window;
}else {
dfs(document.body, item);
}
const { node } = item;
// 没有找到符合要求的元素
if(!node) return;
switch(item.type) {
case 'scrollTo': // 滚动
node.scrollTo({
...item.scroll,
behavior: 'smooth'
});
break;
default: // 其他事件
node[item.type]();
break;
}
});
}
~~~
  scrollTo() 是一个滚动的方法,smooth 是一种平滑选项,奇怪的是,当我去掉此选项时,滚动就无法完成了。
**4)截图**
  本来是计划用脚本来实现截图的,可选的库是[dom-to-image](https://github.com/tsayen/dom-to-image)和[html2canvas](https://html2canvas.hertzen.com/)。
  但是测试下来得到的截图结果都不是很理想,于是就仍然采用 puppeteer 来实现截图。
  先将行为脚本注入,然后等几秒,最后再截图。这种截图得到的结果比较准确,但就是执行过程有点慢,经常需要十几秒甚至更长。
~~~
await page.evaluate(async () => {
const scrpt = document.createElement("script");
scrpt.src = "xx.js";
document.body.appendChild(scrpt);
});
await page.waitForTimeout(2000);
await page.screenshot({
path: `xx/1.png`,
type: "png"
});
~~~
  两张截图的对比可以通过[pixelmatch](https://github.com/mapbox/pixelmatch)完成,下面是官方提供的 node.js 使用示例,[pngjs](https://github.com/lukeapage/pngjs)是一个 png 图像编解码器。
~~~
const fs = require('fs');
const PNG = require('pngjs').PNG;
const pixelmatch = require('pixelmatch');
const img1 = PNG.sync.read(fs.readFileSync('img1.png'));
const img2 = PNG.sync.read(fs.readFileSync('img2.png'));
const {width, height} = img1;
const diff = new PNG({width, height});
pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.1});
fs.writeFileSync('diff.png', PNG.sync.write(diff));
~~~
*****
> 原文出处:
[博客园-Node.js躬行记](https://www.cnblogs.com/strick/category/1688575.html)
[知乎专栏-Node.js躬行记](https://zhuanlan.zhihu.com/pwnode)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020