51Testing软件测试论坛

标题: UI 自动化的页面对象管理工具之实现思路 [打印本页]

作者: lsekfe    时间: 2022-7-12 11:13
标题: UI 自动化的页面对象管理工具之实现思路
基本架构
[attach]138966[/attach]
如图所示,该工具作为 vscode 插件,因为要跟 webpage 的 dom 进行双向沟通,我们将借用 webdriver 里的 js 执行器来进行通讯。
  加载插件
  我们需要启动一个 chromedriver session, 同时加载一个 chrome 插件, 这插件其是基于 selenium IDE recorder 的一个改造版本。
  1. //driver = new Builder().forBrowser('chrome').build();
  2.   const builder = new Builder().withCapabilities({
  3.       browserName: 'chrome',
  4.       'goog:chromeOptions': {
  5.       // Don't set it to headless as extensions dont work in general
  6.       // when used in headless mode
  7.       args: [`load-extension=${path.join(__dirname + '../../build')}`],
  8.       },
  9.   })
  10.   driver = await builder.build()
复制代码
然后在每个 dom 里会有下面几个方法:
  1. //在dom中我们将通过给window 对象绑定新函数的方式注入我们需要调用的方法
  2.   window.__side.selectElement = async callback => {
  3.     await window.__side.postMessage(window, {
  4.       action: 'select',
  5.     }).then(callback)
  6.   }
  7.   window.__side.generateElement = async (callback, options) => {
  8.     await window.__side.postMessage(window, {
  9.       action: 'generateElement',
  10.       builderOptions: options
  11.     }).then(callback)
  12.   }
  13.   window.__side.generateElements = async (callback, options) => {
  14.     await window.__side.postMessage(window, {
  15.       action: 'generateElements',
  16.       builderOptions: options
  17.     }).then(callback)
  18.   }
  19.   window.__side.generateAllElements = async (callback, options) => {
  20.     await window.__side.postMessage(window, {
  21.       action: 'generateAllElements',
  22.       builderOptions: options
  23.     }).then(callback)
  24.   }
复制代码
vscode 调用
  在 PO-Manager 中我们将通过 jsExecutor 去向对应的 web 页面中执行 js 脚本,你可能会好奇这里为啥要用 executeAsyncScript 而不是 executeScript, 并且还有个 callback,这个其实是因为我们选择页面元素是一个异步过程,所以需要 callback 来保证正确的返回。
  1. executeAsyncScript 的用法可以参考这里:
  2.   async selectElement(): Promise<string>  {
  3.       await this.showBrowser()
  4.       return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1];
  5.                                               window.__side.selectElement(callback);`)
  6.                   .then(value => {
  7.                       return value
  8.                   })
  9.   }
  10.   async generateElement(): Promise<string>  {
  11.       await this.showBrowser()
  12.       let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}
  13.       return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1];
  14.                                               window.__side.generateElement(callback, arguments[0]);`, options)
  15.                   .then(value => {
  16.                       return value
  17.                   })
  18.   }
  19.   async generateElements(): Promise<string>  {
  20.       await this.showBrowser()
  21.       let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}
  22.       return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1];
  23.                                               window.__side.generateElements(callback, arguments[0]);`, options)
  24.                   .then(value => {
  25.                       return value
  26.                   })
  27.   }
  28.   async generateAllElements(): Promise<string>  {
  29.       let options = {pascalCase: this.pascalCase, preserveConsecutiveUppercase: this.preserveConsecutiveUppercase}
  30.       return this.driver.executeAsyncScript(`var callback = arguments[arguments.length - 1];
  31.                                               window.__side.generateAllElements(callback, arguments[0]);`, options)
  32.                   .then(value => {
  33.                       return value
  34.                   })
  35.   }
复制代码
 选中元素
[attach]138967[/attach]
[attach]138968[/attach]
从上图中我们可以看到,当我们想添加元素对象的时候,我们要去选择一个页面元素,我们得要想页面注入 mousemove, click, mouseout 的事件监听,并且要有 highlight 的效果显示我们 focus 元素:
  注入监听
  1. //TargetSelector
  2.     constructor(callback, cleanupCallback) {
  3.       this.callback = callback
  4.       this.cleanupCallback = cleanupCallback
  5.       // Instead, we simply assign global content window to this.win
  6.       this.win = window
  7.       const doc = this.win.document
  8.       const div = doc.createElement('div')
  9.       div.setAttribute('style', 'display: none;')
  10.       //doc.body.insertBefore(div, doc.body.firstChild)
  11.       doc.body.appendChild(div)
  12.       this.div = div
  13.       this.e = null
  14.       this.r = null
  15.       if (window === window.top) {
  16.         this.showBanner(doc)
  17.         doc.body.insertBefore(this.banner, div)
  18.       }
  19.       doc.addEventListener('mousemove', this, true)
  20.       doc.addEventListener('click', this, true)
  21.       doc.addEventListener('mouseout', this, true)
  22.     }
复制代码
处理 mouseover 和 click 事件:
  1. handleEvent(evt) {
  2.       switch (evt.type) {
  3.         case 'mousemove':
  4.           this.highlight(evt.target.ownerDocument, evt.clientX, evt.clientY)
  5.           break
  6.         case 'click':
  7.           if (evt.button == 0 && this.e && this.callback) {
  8.             this.callback(this.e, this.win)
  9.           } //Right click would cancel the select
  10.           evt.preventDefault()
  11.           evt.stopPropagation()
  12.           this.cleanup()
  13.           break
  14.         case 'mouseout':
  15.           this.removeHighlight()
  16.           this.e = null
  17.           break
  18.       }
  19.     }
复制代码
上面部分的 js 都是以 content-script 的形式注入的,但我们调用 js executor 的时候只能去执行在 dom 底下的 function,这时候在里 dom 就只能通过 postMessage 的方式了来调用 content-script 的方法了。
  content-script 介绍具体可以参考chrome 插件开发
  如何生成一个元素对象
 "新闻Link": {
          "type": "xpath",
          "locator": "(//a[contains(text(),'新闻')])[1]"
      }

当我们选中一个元素后,要生成上面一个对象,我们需要处理 2 个部分
  · 定位器
  下面的代码是按照以找到 id 为唯一标识,然后生成相对路径的 xpath, 这是最通用的策略,当然对于 link 或者 button 我们也可以以文字显示为标识。
  更多的定位器生成方法,可以查看这里的 源码:
  1. LocatorBuilders.add('xpath:idRelative', function xpathIdRelative(e) {
  2.     let path = ''
  3.     let current = e
  4.     while (current != null) {
  5.       if (current.parentNode != null) {
  6.         path = this.relativeXPathFromParent(current) + path
  7.         if (
  8.           1 == current.parentNode.nodeType && // ELEMENT_NODE
  9.           current.parentNode.getAttribute('id')
  10.         ) {
  11.           return this.preciseXPath(
  12.             '//' +
  13.               this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +
  14.               '[@id=' +
  15.               this.attributeValue(current.parentNode.getAttribute('id')) +
  16.               ']' +
  17.               path,
  18.             e
  19.           )
  20.         }
  21.         else if(current.parentNode.nodeName.toLowerCase() == 'body'){
  22.           return this.preciseXPath(
  23.             '//body' + path,
  24.             e
  25.           )
  26.         }
  27.       } else {
  28.         return null
  29.       }
  30.       current = current.parentNode
  31.     }
  32.     return null
  33.   })
复制代码
生成元素名 基本上选择了所见即所得的策略,优先选择显示文字,然后 id, class 次之, 这个当然在设定中可以配置。
  1. buildName(element){
  2.     if(element.tagName == "svg") {
  3.       element = element.parentElement
  4.     }
  5.     let attrList = ["id", "text", "class"]
  6.     let originalText = this.nameCandidate(element, attrList)
  7.     let name = ""
  8.     let nameArr = (typeof originalText == "string" && originalText.match(/[a-zA-Z0-9\u4e00-\u9fa5]+/g)) || ["DefaultElement"]
  9.     for(const n of nameArr){
  10.       if(name.length >= 30) break
  11.       name += n + ' '
  12.     }
  13.     name = camelcase(name, this.options)
  14.     name = this.append(element, name)
  15.     return name
  16.   }
复制代码
元素筛选
  上面是针对单个元素,如果我们要选择批量添加元素或者整个页面添加的时候,我们会遇到一个问题,dom 元素太多,那我们该如何筛选呢?
[attach]138972[/attach]
通过对 dom 的研究发现,我们大部分时候,只需要包含文字的叶节点,而且 table select 之类的复杂元素,我们也只需要加入这个父节点就行。
  所以针对这个,我用 xpath 做了如下的过滤规则
  1. 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 插入到编辑器就行了。
  1. function generateElements(options){
  2.     return new Promise(res => {
  3.       new TargetSelector(function(element, win) {
  4.         if (element && win) {
  5.           //TODO: generateElements
  6.           let elements = {}
  7.           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)]`
  8.           let elementList = locatorBuilders.findElements(xpathFilter, element)
  9.           for(const ele of elementList){
  10.             if(!isDisplayed(ele)) continue
  11.             const target = locatorBuilders.build(ele)
  12.             nameBuilder.setBuilderOptions(options)
  13.             const elementName = nameBuilder.buildName(ele)
  14.             elements[elementName] = {
  15.               type: target.slice(0,target.indexOf('=')),
  16.               locator: target.slice(target.indexOf('=') + 1)
  17.             }
  18.           }
  19.           if (elements) {
  20.             res(JSON.stringify(elements))
  21.           }
  22.         }
  23.       })
  24.     })
  25.   }
复制代码
下面是一个添加选中区域内元素的例子:
[attach]138973[/attach]
总结一下:
  1. selenium 启动 browser 的时候加载了一个插件,存放一些常驻的 js 方法 (比如元素选择,生成定位器,元素名称等等)。
  2. 当用户选择元素时,通过 driver.executeAsyncScript 这个方法调用后台 js。
  3. new TargetSelector 这个方法会网页注入 js mouseover 和 click 的监听, 当用户 click 的时候,选择的元素然后通过 locatorBuilders.build(element) 生成对应的定位器。
  4. 生成定位器的方式主要是基于当前元素和一些固定属性之间的反推,判定某些属性可以唯一定位到该元素即可。
  5. 然后给元素生成合适的名称,最后组成 json 返回给 vscode 插件,生成 json 插入到编辑器中。














欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2