原文由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个局部
- 定位器
上面的代码是依照以找到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})
- 生成元素名
基本上抉择了所见即所得的策略,优先选择显示文字,而后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)) } } }) })}
上面是一个增加选中区域内元素的例子:
总结一下:
- selenium 启动browser的时候加载了一个插件,寄存一些常驻的js 办法(比方元素抉择,生成定位器,元素名称等等)
- 当用户抉择元素时,通过driver.executeAsyncScript这个办法调用后盾js
- new TargetSelector 这个办法会网页注入js mouseover 和click的监听, 当用户click的时候,抉择的元素而后通过locatorBuilders.build(element) 生成对应的定位器
- 生成定位器的形式次要是基于以后元素和一些固定属性之间的反推,断定某些属性能够惟一定位到该元素即可
- 而后给元素生成适合的名称,最初组成json 返回给vscode 插件,生成json 插入到编辑器中
最初欢送大家star 和 fork,一起来改良。
https://github.com/zzhengjian...
原文由alex发表于TesterHome社区网站,点击原文链接可与作者间接交换
想理解更多关开源工具,与更多开源我的项目作者间接交换?
欢送关注第十届中国互联网测试开发大会(MTSC 2002 上海)· 开源专场 >>>