原文由alex发表于TesterHome社区网站,点击原文链接可与作者直接交流

本文将介绍下 UI 自动化的页面对象管理工具PO-Manager之实现思路:
更多PO-Manager的介绍,可查看上篇《UI 自动化的页面对象管理神器 PO-Manager》

基本架构

如图所示,该工具作为vscode 插件,因为要跟webpage的dom 进行双向沟通,我们将借用webdriver 里的js 执行器来进行通讯。

加载插件

我们需要启动一个chromedriver session, 同时加载一个chrome插件, 这插件其是基于selenium IDE recorder的一个改造版本,

   //driver = new Builder().forBrowser('chrome').build();const builder = new Builder().withCapabilities({browserName: 'chrome','goog:chromeOptions': {// Don't set it to headless as extensions dont work in general// when used in headless modeargs: [`load-extension=${path.join(__dirname + '../../build')}`],},})driver = await builder.build()

然后在每个dom 里会有下面几个方法:

//在dom中我们将通过给window 对象绑定新函数的方式注入我们需要调用的方法
window.__side.selectElement = async callback => {await window.__side.postMessage(window, {action: 'select',}).then(callback)
}window.__side.generateElement = async (callback, options) => {await window.__side.postMessage(window, {action: 'generateElement',builderOptions: options}).then(callback)
}window.__side.generateElements = async (callback, options) => {await window.__side.postMessage(window, {action: 'generateElements',builderOptions: options}).then(callback)
}window.__side.generateAllElements = async (callback, options) => {await window.__side.postMessage(window, {action: 'generateAllElements',builderOptions: options}).then(callback)
}

vscode 调用

在PO-Manager 中我们将通过jsExecutor 去向对应的web页面中执行js脚本,你可能会好奇这里为啥要用executeAsyncScript 而不是executeScript, 并且还有个callback,这个其实是因为我们选择页面元素是一个异步过程,所以需要callback 来保证正确的返回。
executeAsyncScript的用法可以参考这里:

   async selectElement(): Promise<string>  {await this.showBrowser()return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1]; window.__side.selectElement(callback);`).then(value => {return value})}async generateElement(): Promise<string>  {await this.showBrowser()let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1]; window.__side.generateElement(callback, arguments[0]);`, options).then(value => {return value})}async generateElements(): Promise<string>  {await this.showBrowser()let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1]; window.__side.generateElements(callback, arguments[0]);`, options).then(value => {return value})}async generateAllElements(): Promise<string>  {let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1]; window.__side.generateAllElements(callback, arguments[0]);`, options).then(value => {return value})}

选中元素


从上图中我们可以看到,当我们想添加元素对象的时候,我们要去选择一个页面元素,我们得要想页面注入mousemove, click, mouseout的事件监听,并且要有highlight的效果显示我们focus 元素:

注入监听

//TargetSelectorconstructor(callback, cleanupCallback) {this.callback = callbackthis.cleanupCallback = cleanupCallback// Instead, we simply assign global content window to this.winthis.win = windowconst doc = this.win.documentconst div = doc.createElement('div')div.setAttribute('style', 'display: none;')//doc.body.insertBefore(div, doc.body.firstChild)doc.body.appendChild(div)this.div = divthis.e = nullthis.r = nullif (window === window.top) {this.showBanner(doc)doc.body.insertBefore(this.banner, div)}doc.addEventListener('mousemove', this, true)doc.addEventListener('click', this, true)doc.addEventListener('mouseout', this, true)}

处理mouseover和click事件

handleEvent(evt) {switch (evt.type) {case 'mousemove':this.highlight(evt.target.ownerDocument, evt.clientX, evt.clientY)breakcase 'click':if (evt.button == 0 && this.e && this.callback) {this.callback(this.e, this.win)} //Right click would cancel the selectevt.preventDefault()evt.stopPropagation()this.cleanup()breakcase 'mouseout':this.removeHighlight()this.e = nullbreak}}

上面部分的js都是以content-script的形式注入的,但我们调用js executor的时候只能去执行在dom 底下的 function,这时候在里dom 就只能通过postMessage 的方式了来调用content-script的方法了。
content-script介绍具体可以参考chrome插件开发

如何生成一个元素对象

"新闻Link": {"type": "xpath","locator": "(//a[contains(text(),'新闻')])[1]"}

当我们选中一个元素后,要生成上面一个对象,我们需要处理2个部分

  1. 定位器

下面的代码是按照以找到id 为唯一标识,然后生成相对路径的xpath, 这是最通用的策略,当然对于link或者button 我们也可以以文字显示为标识。
更多的定位器生成方法,可以查看这里的 源码:

LocatorBuilders.add('xpath:idRelative', function xpathIdRelative(e) {let path = ''let current = ewhile (current != null) {if (current.parentNode != null) {path = this.relativeXPathFromParent(current) + pathif (1 == current.parentNode.nodeType && // ELEMENT_NODEcurrent.parentNode.getAttribute('id')) {return this.preciseXPath('//' +this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +'[@id=' +this.attributeValue(current.parentNode.getAttribute('id')) +']' +path,e)}else if(current.parentNode.nodeName.toLowerCase() == 'body'){return this.preciseXPath('//body' + path,e)}} else {return null}current = current.parentNode}return null
})
  1. 生成元素名
    基本上选择了所见即所得的策略,优先选择显示文字,然后id, class次之, 这个当然在设定中可以配置
  buildName(element){if(element.tagName == "svg") {element = element.parentElement}let attrList = ["id", "text", "class"]let originalText = this.nameCandidate(element, attrList)let name = ""let nameArr = (typeof originalText == "string" && originalText.match(/[a-zA-Z0-9\u4e00-\u9fa5]+/g)) || ["DefaultElement"]for(const n of nameArr){if(name.length >= 30) breakname += n + ' '}name = camelcase(name, this.options)name = this.append(element, name)return name}

元素筛选

上面是针对单个元素,如果我们要选择批量添加元素或者整个页面添加的时候,我们会遇到一个问题,dom 元素太多,那我们该如何筛选呢?

通过对dom 的研究发现,我们大部分时候,只需要包含文字的叶节点,而且table select 之类的复杂元素,我们也只需要加入这个父节点就行。
所以针对这个,我用xpath 做了如下的过滤规则

xpath=.//*[not(ancestor::table)][normalize-space(translate(text(),' ',' '))][not(ancestor::select)][not(self::sup)][not(self::iframe)][not(self::frame)][not(self::script)]|.//input[not(ancestor::table)][@type!='hidden']|(.//img|.//select|.//i|.//a|.//h1|.//h2|.//h3|.//h4)[not(ancestor::table)]

然后再对元素进行可见性筛选,基本就能满足大部分的需求了。
最后对每个元素生成定位器和元素名,然后返回到PO-Manager 插件并转化成json 插入到编辑器就行了

function generateElements(options){return new Promise(res => {new TargetSelector(function(element, win) {if (element && win) {//TODO: generateElementslet elements = {}let xpathFilter = `xpath=.//*[not(ancestor::table)][normalize-space(translate(text(),' ',' '))][not(ancestor::select)][not(self::sup)][not(self::iframe)][not(self::frame)][not(self::script)]|.//input[not(ancestor::table)][@type!='hidden']|(.//img|.//select|.//i|.//a|.//h1|.//h2|.//h3|.//h4)[not(ancestor::table)]`let elementList = locatorBuilders.findElements(xpathFilter, element)for(const ele of elementList){if(!isDisplayed(ele)) continueconst target = locatorBuilders.build(ele)nameBuilder.setBuilderOptions(options)const elementName = nameBuilder.buildName(ele)elements[elementName] = {type: target.slice(0,target.indexOf('=')),locator: target.slice(target.indexOf('=') + 1)}}if (elements) {res(JSON.stringify(elements))}}})})
}

下面是一个添加选中区域内元素的例子:

总结一下:

  1. selenium 启动browser的时候加载了一个插件,存放一些常驻的js 方法(比如元素选择,生成定位器,元素名称等等)
  2. 当用户选择元素时,通过driver.executeAsyncScript这个方法调用后台js
  3. new TargetSelector 这个方法会网页注入js mouseover 和click的监听, 当用户click的时候,选择的元素然后通过locatorBuilders.build(element) 生成对应的定位器
  4. 生成定位器的方式主要是基于当前元素和一些固定属性之间的反推,判定某些属性可以唯一定位到该元素即可
  5. 然后给元素生成合适的名称,最后组成json 返回给vscode 插件,生成json 插入到编辑器中

最后欢迎大家star 和 fork,一起来改进。
https://github.com/zzhengjian/PO-Manager

原文由alex发表于TesterHome社区网站,点击原文链接可与作者直接交流


想了解更多关开源工具,与更多开源项目作者直接交流?
欢迎关注第十届中国互联网测试开发大会(MTSC 2002 上海)· 开源专场 >>>

UI 自动化的页面对象管理工具之实现思路相关推荐

  1. KingbaseES数据库对象管理工具

    目录 1. 简介 2. 启动数据库对象管理工具 3. 管理和配置数据库服务器 3.1. 实例管理 3.2. 数据库管理 3.3. 模式管理 4. 对象管理 5. 安全管理 5.1. 用户管理 5.2. ...

  2. 数据库人大金仓KingbaseES 数据库对象管理工具连接错误(实例创建失败)问题解决办法

    目录 特别注意!需要跟下图一致! 解决方案一 解决方案二 解决方案三 可能遇见的问题 错误原因 最终效果 特别注意!需要跟下图一致! 然后检查!如果无报错再点击确定,否则连接不上数据库. 经常性的报错 ...

  3. 在使用数据库对象管理工具新建数据库时,默认自带的模式有哪些?  A. public   B. sys_catalog   C. exam   D. sysaudit...

    在使用数据库对象管理工具新建数据库时,默认自带的模式有A. public和B. sys_catalog.

  4. 自动化集成:Jenkins管理工具详解

    前言:该系列文章,围绕持续集成:Jenkins+Docker+K8S相关组件,实现自动化管理源码编译.打包.镜像构建.部署等操作:本篇文章主要描述Jenkins基础用法. 一.Jenkins安装 1. ...

  5. ETL自动化运维调度管理工具 TASKCTL 流程文件系统

    模块信息组织思路 模块作为流程核心信息的基本组织单位,同时也是用户开发设计流程的主要对象.为了更好地设计流程,首先需要深入理解模块信息的组织思路. TASKCTL 流程以模块为单位的流程核心信息组织思 ...

  6. 网易 UI 自动化工具 Airtest 浅用记录

    一 使用目的 该工具主要是面向游戏UI测试基于图像识别,如游戏框架unity,Cocos-js以及网易内部的游戏框架 同时也支持原生Android App 的基于元素识别的UI自动化测试. 本文主要使 ...

  7. python ui自动化配置文件,python UI自动化实战记录八:添加配置

    添加配置文件写入测试地址等,当环境切换时只需修改配置文件即可. 1 在项目目录下添加文件 config.ini 写入: [Domain] domain = http://test.domain.cn ...

  8. pythonWeb UI自动化最流行的工具 解放双手 双手打字以示清白!

    Selenium WebDriver是Web UI自动化最流行的工具之一.没有比Python更好的补充它来自动化广泛的Web应用程序.因此,我们提供了这个Selenium Webdriver Pyth ...

  9. Web UI自动化测试之Selenium工具篇

    本文大纲截图: 一.自动化测试介绍 1.基本介绍 1.1 自动化 概念: 由机器设备代替人工自动完成指定目标的过程 优点: 1)减少人工劳动力 2)提高工作效率 3)产品规格统一标准 4)规模化(批量 ...

最新文章

  1. 南加大提出NeROIC:还有什么不能渲染的?重建效果太强悍了
  2. 《认知突围》做复杂时代的明白人,读书分享
  3. rtmp/rtsp/hls公网测试地址
  4. 程序员最喜欢用的在线IDE代码编译器,什么?你竟然不知道!
  5. [团队项目3.0]Scrum团队成立
  6. python dataframe遍历_对Python中DataFrame按照行遍历的方法
  7. CComPtr对象作为参数进行 1.值传递 2.引用传递 3.做为返回值的注意事项
  8. linux下使用tc工具模拟网络延迟和丢包
  9. linux如何杀死进程最快,如何在Linux系统中杀掉内存消耗最大的进程?
  10. 服务器LCD显示面板,DELL服务器2950的错误代码表(前LCD面板)
  11. gis怎么提取水系_利用ArcGIS水文分析工具提取河网
  12. Apple和Ruby近况:Rails的iPhone配置实用工具和Ruby的SproutCore工具
  13. 计算机主板自动重启,电脑自动重启频繁是主板的问题吗
  14. 解决Android手机人民币符号--¥显示问题
  15. 从强化学习的角度看alphago与MCTS
  16. 跟我学c++中级篇——decay
  17. Swing绝对布局之setBounds
  18. 外卖订单语音通知功能如何实现?(附外卖订单语音通知模板)
  19. 目前常用 心电数据库ECG:MITBIH,AHA,CSE,ST-T,PTB,PAF 详细介绍+下载
  20. Java求两点的中点坐标_计算两点坐标距离与中点坐标

热门文章

  1. 电脑计算机界面打开后无法缩小,技术编辑演示win10系统iE网页界面大小无法缩放的办法...
  2. python去掉两边空格,Python去除字符串两端空格的方法
  3. 百度平台上的网络舆情信息怎么搜查的方法
  4. c++(最大公约数)
  5. python的flask框架实现的小型二手商城
  6. transformers的beam_search
  7. 中科院最新通告:弃用影响因子!将替以「期刊超越指数」发布期刊分区表......
  8. 【Hive】快速入门~
  9. Linux之父:我们都老了,但Linux维护后继无人
  10. 数据分析案例-基于PCA主成分分析法对葡萄酒数据进行分析