使用 GPT4V+AI Agent 做自动 UI 测试的探索
一、背景
从 Web 诞生之日起,UI 自动化就成了测试的难点,到现在近 30 年,一直没有有效的手段解决Web UI测试的问题,尽管发展了很多的 webdriver 驱动,图片 diff 驱动的工具,但是这些工具的投入产出比一直被质疑,自动化率越多维护成本越高,大部分都做着就放弃了,还有一部分在做与不做间纠结。
本文结合一些开源的项目探索使用GPT 自动做 UI 测试的可能性。
二、方案选型
当前UI 的主要问题:一个是通过 Webdriver 控制浏览器执行,这些工具都需要先查找到对应元素的 Elements,无论是录制的还是自己编写的在面对 UI 变化,元素变化时都需要耗费很大的精力去重新识别,解析 Dom 查找,这个工作乏味且没有效率;另一种是通过图像进行点击,比如 Sikuli 这种工具,主要的问题也是复用性较差,换个分辨率的机器或者图片发生少的改动就不可用。
使用 GPT 做 UI 测试尝试了两种方案:
第一种将 Html 元素投喂给 GPT,主要方法获取 Html代码,对 Html 做初步缩减处理,再做向量化,然后喂给 GPT4 自动生成 Webdriver 驱动脚本,效果一般,而且因为 Html 比较大,Token 的消耗很大。
第二种思路是让 GPT 像人一样思考和测试,比如一个人打开一个网页后,他通过眼睛看到的页面文字或图标,然后用手完成点击和输入的操作,最后通过页面的弹窗或者文字来识别是否有错误,这几个动作通过大脑统一协调。
这里主要介绍第二种.
三、新方案实践
1.新方案简介
新的方案主要结合 Playwright,SoM视觉标记,GPT4Vison,GPT4,AutoGen来实现。主要的原理
通过 Playwright进行浏览器操作,包括页面图像的获取、浏览器的各种操作,相当于‘‘手’’;
进行SoM 视觉数据标记,因为 GPT4Vison 在进行页面原始识别时并不是很准确,参考微软的论文可以通过视觉标记的手段来辅助 GPT4V 识别,相当于“眼睛”。
通过GPT4+AutoGen 将这些步骤串起来实现协调控制,相当于“大脑”。
2.主要架构

3.实现步骤
3.1 使用 Playwright 注入 JS
browser = playwright.chromium.launch(channel="chrome",headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://oa.jd.com/")
inject_js ="./pagemark.js"
withopen(inject_js,'r')asfile:
content =file.read()
page.evaluate(f"{content}")
3.2 SoM 视觉提示标记
如前文提到的 GPT4V 并不能有效的识别 Web 的元素,所以在使用 GPT4V 之前进行图像标记,图像标记现在有两种方式,一种是通过 AI 识别图片进行标记,这种主要利用在对静态图片图像的识别,对于 Web 页面的标记,我们可以采用注入 JS 修改页面元素的方式来标记。这里通过在浏览器中注入 pagemark.js,利用 Playwright 执行 js 函数来完成页面的标记,该 JS 能够完成标准的coco annotation的标注。
// DOM Labelerlet labels =[];functionunmarkPage(){
for(const label of labels){
document.body.removeChild(label);
}
labels =[];
}
functionmarkPage(){
unmarkPage();
var bodyRect = document.body.getBoundingClientRect();
var items =Array.prototype.slice.call(
document.querySelectorAll('*')
).map(function(element){
var vw = Math.max(document.documentElement.clientWidth ||0, window.innerWidth ||0);
var vh = Math.max(document.documentElement.clientHeight ||0, window.innerHeight ||0);
var rects =[...element.getClientRects()].filter(bb=>{
var center_x = bb.left + bb.width /2;
var center_y = bb.top + bb.height /2;
var elAtCenter = document.elementFromPoint(center_x, center_y);
return elAtCenter === element || element.contains(elAtCenter)
}).map(bb=>{
const rect ={
left: Math.max(0, bb.left),
top: Math.max(0, bb.top),
right: Math.min(vw, bb.right),
bottom: Math.min(vh, bb.bottom)
};
return{
...rect,
width: rect.right - rect.left,
height: rect.bottom - rect.top
}
});
var area = rects.reduce((acc, rect)=> acc + rect.width * rect.height,0);
return{
element: element,
include:
(element.tagName ==="INPUT"|| element.tagName ==="TEXTAREA"|| element.tagName ==="SELECT")||
(element.tagName ==="BUTTON"|| element.tagName ==="A"||(element.onclick !=null)|| window.getComputedStyle(element).cursor =="pointer")||
(element.tagName ==="IFRAME"|| element.tagName ==="VIDEO")
,
area,
rects,
text: element.textContent.trim().replace(/\s{2,}/g,' ')
};
}).filter(item=>
item.include &&(item.area >=20)
);
// Only keep inner clickable items
items = items.filter(x=>!items.some(y=> x.element.contains(y.element)&&!(x == y)))
// Function to generate random colors
functiongetRandomColor(){
var letters ='0123456789ABCDEF';
var color ='#';
for(var i =0; i <6; i++){
color += letters[Math.floor(Math.random()*16)];
}
return color;
}
// Lets create a floating border on top of these elements that will always be visible
items.forEach(function(item, index){
item.rects.forEach((bbox)=>{
newElement = document.createElement("div");
var borderColor =getRandomColor();
newElement.style.outline =`2px dashed ${borderColor}`;
newElement.style.position ="fixed";
newElement.style.left = bbox.left +"px";
newElement.style.top = bbox.top +"px";
newElement.style.width = bbox.width +"px";
newElement.style.height = bbox.height +"px";
newElement.style.pointerEvents ="none";
newElement.style.boxSizing ="border-box";
newElement.style.zIndex =2147483647;
// newElement.style.background = `${borderColor}80`;
// Add floating label at the corner
var label = document.createElement("span");
label.textContent = index;
label.style.position ="absolute";
label.style.top ="-19px";
label.style.left ="0px";
label.style.background = borderColor;
label.style.color ="white";
label.style.padding ="2px 4px";
label.style.fontSize ="12px";
label.style.borderRadius ="2px";
newElement.appendChild(label);
document.body.appendChild(newElement);
labels.push(newElement);
console.log(index)
});
})
}
以某系统为例,标注后的效果如下图:

将标记元素的位置编号保存到本地coco.json
{
"1":{"x":"10","y":"40","w":"8","h":"10","center_x":"30","center_y":"13"},
"2":{"x":"20","y":"40","w":"16","h":"10","center_x":"30","center_y":"21"},
"10":{"x":"40","y":"80","w":"16","h":"10","center_x":"80","center_y":"21"}}
其中 center_x, center_y 为元素的中心点坐标
3.3 GPT4V 识别
Prompt
我上传了一张网页截图,页面元素已经使用COCO Annotator进行了数字标记。
请分析这张图片,并识别出网页的用途以及各个标记元素的功能。基于你的分析,请生成一个规划路径来完成以下任务:
- 输入ERP搜索。
- 检查列表是否为空
请使用图片中的数字标记来描述完成任务的具体步骤。
GPT4V Response
这张图片显示的是一个具有多个数字标记的网页截图。根据这些标记,我们可以推测出网页的用途以及各个元素的功能。
为了完成您提出的任务,我们可以按照以下步骤操作:
\n\n1. 使用标记为10的搜索框输入ERP搜索词。这个搜索框的标签显示为“搜索框输入erp”,表明它用于输入搜索词。
\n\n2. 点击标记为17的搜索按钮。这个按钮的标签是“搜索”,表明它用于提交搜索请求。
\n\n3. 检查列表是否为空。这个步骤可能需要查看标记为19的区域,这个区域被标记为“列表内容”,如果这个区域没有显示任何内容或者有提示信息显示列表为空,则说明搜索结果为空。
\n\n以 上步骤基于图片中的数字标记和元素标签来描述完成任务的具体操作。"
提取元素坐标
利用正则表达式从GPT4V 的 response 中提取查询到的元素的 bbox id= 10,17,19, 结合在 SoM 标记中记录的 json 文件,找到编号 10 的元素坐标"10":{"x":"40","y":"80","w":"16","h","10","center_x":"80","center_y":"21"}
class GPT4VRecognize
def get_location(self, query):
coco_json='./coco.json'
withopen(coco_json,'r') as file:
content =file.read()
matches=re.findall(r'\d+',gpt4v_response_content)
num=-1
iflen(matches)>0:
num=matches[0]
data = json.loads(json_str)
center_x = data[num]["center_x"]
center_y = data[num]["center_y"]
return center_x,center_y
3.4 Playwright操作页面
Playwright是一个非常强大的操作浏览器的工具,这里因为前面已经通过 GPT4V 识别了图片,所以我们主要通过 坐标 的方式来控制元素,不再需要解析Dom 来控制,主要的包括,点击,双击,鼠标拖动,输入,截图等:
class Actions:
page=None
__init__(self,url):
global page
browser = playwright.chromium.launch(channel="chrome",headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://oa.jd.com/")
def mouse_move(self,x,y):
page.mouse.move(self,x,y)
def screenshot(self):
page.screenshot()
def mouse_click(self,x,y):
page.mouse.click(self,x,y)
def input_text(self,x,y,text):
page.mouse.click(self,x,y)
page.keyboard.type(text)
def mouse_db_click(self,x,y):
def select_option(self,x,y,option):
......
3.5 使用 AutoGen编排
AutoGen是一个代理工具,它能够代理多个 LLM在不同会话间切换,能够自动的调用预定的函数,并协调这些 LLM 完成任务。
在上面的程序中,实现了:眼睛:通过 GPT4V 来识别元素;手:通过 Playwright 来做各种操作;后面需要通过一个大脑来协调手、眼完成任务,这里通过 GPT4+AutoGen 来实现大脑的协同调度。
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
assistant= autogen.AssistantAgent(
name="assistant",
system_message=
"""
You are the orchestrator responsible for completing a task involving the UI window.
Utilize the provided functions to take a screenshot after each action.
Remember to only use the functions you have been given and focus on the task at hand.
""",
llm_config={"config_list": config_list},
)
user_proxy = autogen.UserProxyAgent(
name="brain_proxy",
human_input_mode="NEVER",
code_execution_config={"work_dir":"coding"},
max_consecutive_auto_reply=10,
llm_config={"config_list": config_list},)
action = Actions(url)
gpt4v=GPT4VRecognize()
user_proxy.register_function(
function_map={
"get_location": gpt4v.get_location,
"mouse_move":action.mouse_move,
"screenshot":action.screenshot,
"mouse_click":action.mouse_click,
"mouse_dbclick":action.mouse_dbclick,
"select_option":action.select_option
})
def run(message_ask):
user_proxy.initiate_chat(assistant,message=message_ask)
if __name__ =="__main__":
run("通过 输入erp 'wwww30' 搜索结果,并检查是否返回空列表")
四、问题和后续
1.当前的主要问题
本文主要抛砖引玉,通过一种完全按人类思维进行 UI测试的方式,在实验过程中还有很多问题待解决。
- GPT 在中文语境下识别的不太友好,在实验过程中对中文的 prompt 理解有误,需要不断的优化 prompt,尤其是页面中的中文元素。
- AutoGen 对于处理预定义的动作也会有问题,需要不断调优。
- GPT4V 真的很贵。
2.未来的想法
- 将每次向 GPT4V请求的图像识别做本地化处理,结合现有的一些测试方法,从而减少 Token,缩短执行耗时。
- 面向业务的 GPT需要不断训练,将系统使用手册和一有的 PRD 文档投喂给 GPT,增强 gpt 的系统识别和测试能力。
五、参考
1.Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V
作者:CHO 武小平
来源:京东云开发者社区 转载请注明来源
使用 GPT4V+AI Agent 做自动 UI 测试的探索的更多相关文章
- 在Android Studio中进行单元测试和UI测试
本篇教程翻译自Google I/O 2015中关于测试的codelab,掌握科学上网的同学请点击这里阅读:Unit and UI Testing in Android Studio.能力有限,如有翻译 ...
- Google+ 团队的 Android UI 测试
https://github.com/bboyfeiyu/android-tech-frontier/tree/master/android-blog/Google%2B%20%E5%9B%A2%E9 ...
- 使用uiautomatorviewer和uiautomator来做android的UI测试
来自:http://university.utest.com 作者:Angelos Nakulas (All Authored Courses) 译者:Elaine00 目录 简介 什 ...
- 使用uiautomator做UI测试
转载~~~~~~~~~~~~~~~~~~~~~~~~ 若有侵权,请及时联系本博主,博主将第一时间撤销 在Android 4.1发布的时候包含了一种新的测试工具–uiautomator,uiautoma ...
- [转载]使用uiautomator做UI测试
这个只是单纯的mark一下.还没有认真去研究.鉴于最近也不会做手机的自动化测试,所以留作以后参考吧. 转自: http://blog.chengyunfeng.com/?p=504 在Android ...
- [zhuan]使用uiautomator做UI测试
http://blog.chengyunfeng.com/?p=504 在Android 4.1发布的时候包含了一种新的测试工具–uiautomator,uiautomator是用来做UI测试的.也就 ...
- WWDC15 Session笔记 - Xcode 7 UI 测试初窥
https://onevcat.com/2015/09/ui-testing/ WWDC15 Session笔记 - Xcode 7 UI 测试初窥 Unit Test 在 iOS 开发中已经有足够多 ...
- 记录使用gogs,drone搭建自动部署测试环境
使用gogs,drone,docker搭建自动部署测试环境 Gogs是一个使用go语言开发的自助git服务,支持所有平台 Docker是使用go开发的开源容器引擎 Drone是一个基于容器技术的持续集 ...
- Google推出iOS功能性UI测试框架EarlGrey
经过了一段时间的酝酿后,Google很高兴地宣布了EarlGrey,一款针对于iOS的功能性UI测试框架.诸如YouTube.Google Calendar.Google Photos.Google ...
- 一种新的自动化 UI 测试解决方案 Airtest Project
今天分享一个自动化UI测试工具airtest——一款网易出品的基于图像识别面向游UI测试的工具,也支持原生Android App基于元素识别的UI自动化测试.主要包含了三部分:Airtest IDE. ...
随机推荐
- codeblock安装及汉化教程
1.双击图标 2.弹出如下对话框: 3.单击按钮Next,弹出如下对话框: 4.单击按钮I Agree,弹出如下对话框: 5.单击按钮Next,弹出如下对话框: 6.单击Browse按钮,可以重新设置 ...
- Jenkins持续集成入门到精通(进阶篇)
视频参考:https://www.bilibili.com/video/BV1Vp4y1b7ZN?p=51 1. Jenkins+Docker+SpringCloud持续集成流程说明 大致流程说明: ...
- Python 实现Word转PDF
通过将 Word 文档转换为 PDF,您可以确保文档在不同设备上呈现一致,并防止其他人对文档内容进行非授权修改.此外,在你需要打印文档时,转换为PDF还能确保打印输出的准确性.本文将介绍如何使用Pyt ...
- 5 分钟理解 Next.js SSG (Static Site Generation / Static Export)
5 分钟理解 Next.js SSG (Static Site Generation / Static Export) 在本篇文章中,我们将介绍 Next.js 中的 SSG(静态网站生成)功能,以及 ...
- 其它——paramiko模块的使用
文章目录 paramiko 一 介绍 二 通过用户名密码方式远程执行命令 三 通过用户名密码方式上传下载文件 四 通过公钥私钥远程执行命令 五 通过公钥私钥远程上传下载文件 六 通过私钥字符串远程连接 ...
- 解决因对EFCore执行SQL方法不熟练而引起的问题
前言 本文测试环境:VS2022+.Net7+MySQL 因为我想要实现使用EFCore去执行sql文件,所以就用到了方法ExecuteSqlAsync,然后就产生了下面的问题,首先因为方法接收的参数 ...
- 4款.NET开源的Redis客户端驱动库
前言 今天给大家推荐4款.NET开源免费的Redis客户端驱动库(以下排名不分先后). Redis是什么? Redis全称是REmote DIctionary Service,即远程字典服务.Redi ...
- 一文详解贝叶斯优化(Bayesian Optimization)原理
参考资料: Expected Improvement formula for Bayesian Optimisation 通俗科普文:贝叶斯优化与SMBO.高斯过程回归.TPE 理解贝叶斯优化 A T ...
- 虹科案例 | 石油天然气行业CFD高性能计算解决方案
公司简介 DNV GL 是全球领先的能源.石油和海事行业风险管理及资产绩效提升的软件供应商,主要为客户提供全面的风险管理和各类评估认证服务,认证涉及信息通信技术.汽车及航空天.食品与饮料.医疗等方面. ...
- matlab快速入门笔记
命名规则: clc:清除命令行的所有命令 clear all:清除所有工作区的内容 注释:两个% + 空格 %% matlab的数据类型 1.数字 3 3 * 5 3 / 5 3 + 5 3 - 5 ...