最近在研究[Web自動化測試](https://www.cnblogs.com/strick/p/16892143.html),之前做了些實踐,但效果并不理想。
  對于 QA 來說,公司的網頁交互并不多,用手點點也能滿足。對于前端來說,如果要做成自動化,就得維護一堆的腳本。
  當然,這些腳本也可以 QA 來維護,但前提是得讓他們覺得做這件事的 ROI 很高,依目前的情況看,好像不高。
  所以在想,做一個平臺,在這個平臺中可以保存些數據,并且在旁邊提供個小窗口,呈現要測試的 H5 網頁,如下圖所示(畫圖工具是[excalidraw](https://excalidraw.com/))。
  在修改相關數據后,可以直接看到網頁的變化。
:-: 
  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)獲取的,但是該方法參照的是視口的左上角,也就是說會隨著滾動而改變坐標。
:-: 
  所以就換了一種能更加精確獲取坐標的方法,如下所示,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),歡迎瀏覽。

推薦一款前端監控腳本:[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年
- 2024年
- 日志
- 2020