关于selenium:UI-自动化的页面对象管理工具之实现思路

37次阅读

共计 7009 个字符,预计需要花费 18 分钟才能阅读完成。

原文由 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 mode
        args: [`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 元素:

注入监听

//TargetSelector
  constructor(callback, cleanupCallback) {
    this.callback = callback
    this.cleanupCallback = cleanupCallback

    // Instead, we simply assign global content window to this.win
    this.win = window
    const doc = this.win.document
    const div = doc.createElement('div')
    div.setAttribute('style', 'display: none;')
    //doc.body.insertBefore(div, doc.body.firstChild)
    doc.body.appendChild(div)
    this.div = div
    this.e = null
    this.r = null
    if (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)
        break
      case 'click':
        if (evt.button == 0 && this.e && this.callback) {this.callback(this.e, this.win)
        } //Right click would cancel the select
        evt.preventDefault()
        evt.stopPropagation()
        this.cleanup()
        break
      case 'mouseout':
        this.removeHighlight()
        this.e = null
        break
    }
  }

下面局部的 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 = e
  while (current != null) {if (current.parentNode != null) {path = this.relativeXPathFromParent(current) + path
      if (
        1 == current.parentNode.nodeType && // ELEMENT_NODE
        current.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) break
      name += 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: generateElements
        let 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)) continue
          const 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…

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


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

正文完
 0