使用 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

1browser = playwright.chromium.launch(channel="chrome",headless=False) 2context = browser.new_context() 3page = context.new_page() 4page.goto("http://oa.jd.com/") 5inject_js ="./pagemark.js" 6withopen(inject_js,'r')asfile: 7 content =file.read() 8 page.evaluate(f"{content}") 9 10 11 12

3.2 SoM 视觉提示标记

如前文提到的 GPT4V 并不能有效的识别 Web 的元素,所以在使用 GPT4V 之前进行图像标记,图像标记现在有两种方式,一种是通过 AI 识别图片进行标记,这种主要利用在对静态图片图像的识别,对于 Web 页面的标记,我们可以采用注入 JS 修改页面元素的方式来标记。这里通过在浏览器中注入 pagemark.js,利用 Playwright 执行 js 函数来完成页面的标记,该 JS 能够完成标准的coco annotation的标注。

1// DOM Labelerlet labels =[];functionunmarkPage(){ 2 for(const label of labels){ 3 document.body.removeChild(label); 4 } 5 6 labels =[]; 7 } 8 9 functionmarkPage(){ 10 unmarkPage(); 11 12 var bodyRect = document.body.getBoundingClientRect(); 13 14 var items =Array.prototype.slice.call( 15 document.querySelectorAll('*') 16 ).map(function(element){ 17 var vw = Math.max(document.documentElement.clientWidth ||0, window.innerWidth ||0); 18 var vh = Math.max(document.documentElement.clientHeight ||0, window.innerHeight ||0); 19 20 var rects =[...element.getClientRects()].filter(bb=>{ 21 var center_x = bb.left + bb.width /2; 22 var center_y = bb.top + bb.height /2; 23 var elAtCenter = document.elementFromPoint(center_x, center_y); 24 25 return elAtCenter === element || element.contains(elAtCenter) 26 }).map(bb=>{ 27 const rect ={ 28 left: Math.max(0, bb.left), 29 top: Math.max(0, bb.top), 30 right: Math.min(vw, bb.right), 31 bottom: Math.min(vh, bb.bottom) 32 }; 33 return{ 34 ...rect, 35 width: rect.right - rect.left, 36 height: rect.bottom - rect.top 37 } 38 }); 39 40 var area = rects.reduce((acc, rect)=> acc + rect.width * rect.height,0); 41 42 return{ 43 element: element, 44 include: 45 (element.tagName ==="INPUT"|| element.tagName ==="TEXTAREA"|| element.tagName ==="SELECT")|| 46 (element.tagName ==="BUTTON"|| element.tagName ==="A"||(element.onclick !=null)|| window.getComputedStyle(element).cursor =="pointer")|| 47 (element.tagName ==="IFRAME"|| element.tagName ==="VIDEO") 48 , 49 area, 50 rects, 51 text: element.textContent.trim().replace(/\s{2,}/g,' ') 52 }; 53 }).filter(item=> 54 item.include &&(item.area >=20) 55 ); 56 57 // Only keep inner clickable items 58 items = items.filter(x=>!items.some(y=> x.element.contains(y.element)&&!(x == y))) 59 60 // Function to generate random colors 61 functiongetRandomColor(){ 62 var letters ='0123456789ABCDEF'; 63 var color ='#'; 64 for(var i =0; i <6; i++){ 65 color += letters[Math.floor(Math.random()*16)]; 66 } 67 return color; 68 } 69 70 // Lets create a floating border on top of these elements that will always be visible 71 items.forEach(function(item, index){ 72 item.rects.forEach((bbox)=>{ 73 newElement = document.createElement("div"); 74 var borderColor =getRandomColor(); 75 newElement.style.outline =`2px dashed ${borderColor}`; 76 newElement.style.position ="fixed"; 77 newElement.style.left = bbox.left +"px"; 78 newElement.style.top = bbox.top +"px"; 79 newElement.style.width = bbox.width +"px"; 80 newElement.style.height = bbox.height +"px"; 81 newElement.style.pointerEvents ="none"; 82 newElement.style.boxSizing ="border-box"; 83 newElement.style.zIndex =2147483647; 84 // newElement.style.background = `${borderColor}80`; 85 // Add floating label at the corner 86 var label = document.createElement("span"); 87 label.textContent = index; 88 label.style.position ="absolute"; 89 label.style.top ="-19px"; 90 label.style.left ="0px"; 91 label.style.background = borderColor; 92 label.style.color ="white"; 93 label.style.padding ="2px 4px"; 94 label.style.fontSize ="12px"; 95 label.style.borderRadius ="2px"; 96 newElement.appendChild(label); 97 98 document.body.appendChild(newElement); 99 labels.push(newElement); 100 console.log(index) 101 102 }); 103 }) 104 } 105 106 107 108

以某系统为例,标注后的效果如下图:

将标记元素的位置编号保存到本地coco.json

1{ 2 "1":{"x":"10","y":"40","w":"8","h":"10","center_x":"30","center_y":"13"}, 3 "2":{"x":"20","y":"40","w":"16","h":"10","center_x":"30","center_y":"21"}, 4 "10":{"x":"40","y":"80","w":"16","h":"10","center_x":"80","center_y":"21"}} 5 6 7 8

其中 center_x, center_y 为元素的中心点坐标

3.3 GPT4V 识别

Prompt

1我上传了一张网页截图,页面元素已经使用COCO Annotator进行了数字标记。 2请分析这张图片,并识别出网页的用途以及各个标记元素的功能。基于你的分析,请生成一个规划路径来完成以下任务: 3- 输入ERP搜索。 4- 检查列表是否为空 5请使用图片中的数字标记来描述完成任务的具体步骤。 6 7 8 9

GPT4V Response

1这张图片显示的是一个具有多个数字标记的网页截图。根据这些标记,我们可以推测出网页的用途以及各个元素的功能。 2为了完成您提出的任务,我们可以按照以下步骤操作: 3\n\n1. 使用标记为10的搜索框输入ERP搜索词。这个搜索框的标签显示为“搜索框输入erp”,表明它用于输入搜索词。 4\n\n2. 点击标记为17的搜索按钮。这个按钮的标签是“搜索”,表明它用于提交搜索请求。 5\n\n3. 检查列表是否为空。这个步骤可能需要查看标记为19的区域,这个区域被标记为“列表内容”,如果这个区域没有显示任何内容或者有提示信息显示列表为空,则说明搜索结果为空。 6\n\n以 上步骤基于图片中的数字标记和元素标签来描述完成任务的具体操作。" 7 8 9 10

提取元素坐标

利用正则表达式从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"}

1class GPT4VRecognize 2 def get_location(self, query): 3 coco_json='./coco.json' 4 withopen(coco_json,'r') as file: 5 content =file.read() 6 matches=re.findall(r'\d+',gpt4v_response_content) 7 num=-1 8 iflen(matches)>0: 9 num=matches[0] 10 data = json.loads(json_str) 11 center_x = data[num]["center_x"] 12 center_y = data[num]["center_y"] 13 return center_x,center_y 14 15 16

3.4 Playwright操作页面

Playwright是一个非常强大的操作浏览器的工具,这里因为前面已经通过 GPT4V 识别了图片,所以我们主要通过 坐标 的方式来控制元素,不再需要解析Dom 来控制,主要的包括,点击,双击,鼠标拖动,输入,截图等:

1class Actions: 2 page=None 3 __init__(self,url): 4 global page 5 browser = playwright.chromium.launch(channel="chrome",headless=False) 6 context = browser.new_context() 7 page = context.new_page() 8 page.goto("http://oa.jd.com/") 9 def mouse_move(self,x,y): 10 page.mouse.move(self,x,y) 11 def screenshot(self): 12 page.screenshot() 13 def mouse_click(self,x,y): 14 page.mouse.click(self,x,y) 15 def input_text(self,x,y,text): 16 page.mouse.click(self,x,y) 17 page.keyboard.type(text) 18 def mouse_db_click(self,x,y): 19 def select_option(self,x,y,option): 20 ...... 21 22

3.5 使用 AutoGen编排

AutoGen是一个代理工具,它能够代理多个 LLM在不同会话间切换,能够自动的调用预定的函数,并协调这些 LLM 完成任务。

在上面的程序中,实现了:眼睛:通过 GPT4V 来识别元素;手:通过 Playwright 来做各种操作;后面需要通过一个大脑来协调手、眼完成任务,这里通过 GPT4+AutoGen 来实现大脑的协同调度。

1config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST") 2assistant= autogen.AssistantAgent( 3name="assistant", 4system_message= 5 """ 6 You are the orchestrator responsible for completing a task involving the UI window. 7 Utilize the provided functions to take a screenshot after each action. 8 Remember to only use the functions you have been given and focus on the task at hand. 9 """, 10 llm_config={"config_list": config_list}, 11) 12 user_proxy = autogen.UserProxyAgent( 13 name="brain_proxy", 14 human_input_mode="NEVER", 15 code_execution_config={"work_dir":"coding"}, 16 max_consecutive_auto_reply=10, 17 llm_config={"config_list": config_list},) 18 19action = Actions(url) 20gpt4v=GPT4VRecognize() 21user_proxy.register_function( 22 function_map={ 23 "get_location": gpt4v.get_location, 24 "mouse_move":action.mouse_move, 25 "screenshot":action.screenshot, 26 "mouse_click":action.mouse_click, 27 "mouse_dbclick":action.mouse_dbclick, 28 "select_option":action.select_option 29 }) 30def run(message_ask): 31 user_proxy.initiate_chat(assistant,message=message_ask) 32 33if __name__ =="__main__": 34 run("通过 输入erp 'wwww30' 搜索结果,并检查是否返回空列表") 35 36 37

四、问题和后续

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

2.Microsoft AutoGen

3.GPT-4V-ACT

作者:CHO 武小平

来源:京东云开发者社区 转载请注明来源

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

Docker + selenium + jenkins 并行跑自动化测试

背景目标:使用docker做UI自动化测试并在Jenkins运行调试环境:MocOS运行环境:Linuxubuntudocker涉及工具:docker,selenium,unittest,jenkins基本思路找一个适合做自动化的docker镜像,使用seleniumgrid远程执行selenium。镜像

Katalon 学习笔记(一)

工具介绍:KatalonStudio是一个能提供一整套功能来实现Web,API和Mobile的全自动测试解决方案的自动化测试平台。KatalonStudio构建于开源Selenium和Appium框架之上,通过减少学习和集成这些框架以满足自动化测试需求所需的工作量和专业知识,使团队能够快速开始测试自动化。与其他流行工具

RobotFramework自动化测试之元素定位

前言:最近在做基于RF框架的Web自动化测试,其中涉及到元素的定位,主要用到id、name、xpath、css四中定位方法,尤其后面的两种方法特别有效,可以解决大部分的定位问题。

Parasoft cpptestcli 指令行参数配置

ParasoftCtest  是款功能非常强大的自动化测试工具,能有效提高软件开发效率和代码质量。功能特点如下:    静态代码分析和编码策略实施;  自动代码审查的图形界面和进度跟踪;  自动化的单元测试和回归测试;  代码覆盖率分析,集成通过应用层覆盖跟踪从单元级测试;  结合静态分析、单元测试、代码评审和其他