commit of FAutoTest-version_2.2.2
|
@ -0,0 +1 @@
|
|||
recursive-include fastAutoTest/core/wx/apk *.apk
|
|
@ -0,0 +1,210 @@
|
|||
# FastAutoTest
|
||||
|
||||
> 一个 H5、小程序自动化测试框架
|
||||
|
||||
## 简介
|
||||
|
||||
随着产品业务形态逐渐从 App 延升到微信小程序、微信公众号以及 QQ公众号等,而之前的自动化建设主要是 Native App 为主,全手工测试已无法满足快速增长的业务需求。为提升测试效率和质量,FAutoTest 框架致力于解决 QQ、微信内 UI 的自动化问题(包括微信内 H5页面和小程序、QQ 内 H5页面),提升自动化测试的效率和稳定性。
|
||||
|
||||
FAutoTest 是一款成长中的框架,吸收借鉴了一些 UI 自动化工具的优秀设计理念和思想。如果框架有不足地方,或者你有更好的建议,欢迎提交 Issues 参与贡献。
|
||||
|
||||
### 特性
|
||||
|
||||
FAutoTest 专业服务于微信 H5/小程序的UI自动化测试,提升测试效率与质量。
|
||||
|
||||
* 支持微信 H5页面,能识别常见 H5控件,能获取页面任意内容(常见的如文字、图片、链接等)
|
||||
* 支持小程序内控件识别,操作,页面内容获取等
|
||||
* 支持基础的性能测试监控
|
||||
* 支持安卓 Native 页面组合操作使用
|
||||
* 简单的 API 设计,较低的使用门槛
|
||||
|
||||
### 架构
|
||||
|
||||
1. 整体采用分层设计,API设计方式参考 WebDriver
|
||||
|
||||
2. 整体框架是一个同步阻塞的模型:在一个线程中循环的执行 receive 方法,等待收到 response,发送消息后,阻塞,只有当 receive 方法获得消息时,才会解除阻塞,发送下一条消息,具备超时异常处理机制
|
||||
|
||||
3. 框架内打包了 Python 版本的 UIAutomator,方便在安卓 Native 页面进行操作
|
||||
|
||||
<img src="docs/images/connectedProcess.png" width="500" />
|
||||
|
||||
**User Interface(用户交互层):** 提供给用户所有的界面操作 API(H5界面及小程序界面),使用者不需要关注框架内部实现,只需要关注自身业务逻辑流程(手工用例流程转换成自动化流程)
|
||||
|
||||
**PageOperator(操作解析层):** 主要用于接收和解析用户命令后传递给下层 Engine 层
|
||||
|
||||
**Engine( H5&小程序引擎层):** 将用户命令传输到手机,并返回结果信息。封装 WebSocket 和单线程池,通过WebSocket Debug URL 和浏览器内核建立链接,发送 Json 格式的协议到手机端进行用户指定的操作。
|
||||
|
||||
### 业务流程
|
||||
|
||||
* H5页面/小程序 UI 自动化执行流程
|
||||
|
||||
<img src="docs/images/executedProcess.png" width="400"/>
|
||||
|
||||
* 自动化脚本调用流程
|
||||
|
||||
<img src="docs/images/CallProcess.png" width="500"/>
|
||||
|
||||
## 依赖软件环境
|
||||
|
||||
| 软件 | 软件要求 |
|
||||
| ------------------ | ----------------------- |
|
||||
| Python 版本 | 2.7.x 版本 |
|
||||
| Java JDK 版本 | Java 语言框架(最低1.7) |
|
||||
| Android SDK 版本 | 4.4 及以上版本 |
|
||||
| adb 版本 | 最新版本即可 |
|
||||
| Python IDE 开发环境 | 如 PyCharm CE等 |
|
||||
|
||||
#### 1. Python 版本安装
|
||||
|
||||
下载 & 安装 [Python](https://www.Python.org/downloads/),安装后在终端输入命令 `python -v` 和 `pip list`,能够执行,说明 Python 环境配置成功。
|
||||
|
||||
|
||||
#### 2. Java JDK 版本安装
|
||||
|
||||
下载 & 安装 [Java JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html) 安装后在终端输入命令`java -version`,`java`, `javac`命令能够执行,则 Java 环境配置成功。
|
||||
|
||||
|
||||
#### 3. Android SDK 版本安装
|
||||
|
||||
下载 & 安装 [Android Studio](https://developer.android.com/studio/),然后在里面安装 `Android SDK`
|
||||
|
||||
#### 4. 配置 adb 环境
|
||||
|
||||
安装 Android Studio 后,配置 SDK 环境(若自动安装不成功,可到[手动下载安装](http://developer.android.com/sdk/index.html) ,安装后在终端输入命令 `adb version` 执行有结果,则说明配置成功
|
||||
|
||||
#### 5. IDE 安装
|
||||
|
||||
下载 & 安装 [Pycharm CE](https://www.jetbrains.com/pycharm/download/)
|
||||
|
||||
## FAutoTest 开发环境安装
|
||||
|
||||
| 库名称 | 版本 | 下载地址 |
|
||||
| ---------------- | ------ | ------------------------------------------------ |
|
||||
| uiautomator | 0.3.2 | https://pypi.org/project/uiautomator/#files |
|
||||
| lxml | 4.2.3 | https://pypi.org/project/lxml/4.2.3/#files |
|
||||
| bidict | 0.17.0 | https://pypi.org/project/bidict/#files |
|
||||
| websocket-client | 0.44.0 | https://pypi.org/project/websocket-client/#files |
|
||||
|
||||
* 使用`pip`安装框架所需的第三方库 `uiautomator`、`websocket-client`、`lxml`、`bidict`、`ADBkeyBoard`,如安装`lxml`、`bidict`、`websocket-client`可用`pip`形式安装,如安装`lxml`,
|
||||
|
||||
```
|
||||
pip install lxml
|
||||
```
|
||||
|
||||
* 安装自身框架
|
||||
|
||||
```
|
||||
pip install FAutoTest
|
||||
```
|
||||
|
||||
* 打开微信Debug模式,安装 TBS 内核
|
||||
* 可在微信中打开 X5 调试地址:[http://debugx5.qq.com](http://debugx5.qq.com)
|
||||
* TBS 内核安装地址:[http://debugtbs.qq.com](http://debugtbs.qq.com)
|
||||
* 详情方式见:[http://x5.tencent.com/tbs/guide/debug/faq.html](http://x5.tencent.com/tbs/guide/debug/faq.html)
|
||||
|
||||
## 使用方式
|
||||
|
||||
如何写测试案例,如微信 H5页面,如下所示:
|
||||
|
||||
```Python
|
||||
# coding=utf-8
|
||||
from fastAutoTest.core.h5.h5Engine import H5Driver
|
||||
|
||||
# http://h5.baike.qq.com/mobile/enter.html 从微信进入此链接,首屏加载完后执行脚本
|
||||
if __name__ == '__main__':
|
||||
h5Driver = H5Driver()
|
||||
h5Driver.initDriver()
|
||||
h5Driver.clickElementByXpath('/html/body/div[1]/div/div[3]/p')
|
||||
h5Driver.clickFirstElementByText('白内障')
|
||||
h5Driver.returnLastPage()
|
||||
h5Driver.returnLastPage()
|
||||
print(h5Driver.getElementTextByXpath('/html/body/div[1]/div/div[3]/p'))
|
||||
h5Driver.close()
|
||||
|
||||
```
|
||||
|
||||
1. 从微信初始化 H5页面,如进入 [http://h5.baike.qq.com/mobile/enter.html](http://h5.baike.qq.com/mobile/enter.html)
|
||||
2. 进入页面后找到需要操作的控件的`xpath`,可通过 `chrome:inspect` 找到当前页面,找到控件的`xpath`
|
||||
3. 初始化框架并进行 API 调用,如执行点击控件等
|
||||
4. 关闭框架,执行用例
|
||||
|
||||
QQ 的 H5页面:
|
||||
|
||||
```Python
|
||||
# coding=utf-8
|
||||
from fastAutoTest.core.qq.qqEngine import QQDriver
|
||||
|
||||
# 从动态 -> 动漫进入
|
||||
if __name__ == '__main__':
|
||||
qqDriver = QQDriver()
|
||||
qqDriver.initDriver()
|
||||
qqDriver.clickFirstElementByText('英雄救美,这也太浪漫了')
|
||||
qqDriver.returnLastPage()
|
||||
qqDriver.clickElementByXpath('//*[@id="app"]/div/ul/li[2]')
|
||||
qqDriver.returnLastPage()
|
||||
qqDriver.close()
|
||||
```
|
||||
|
||||
1. 从 QQ 动态,进入动漫 H5页面
|
||||
2. 找到需操作的控件的`xpath`,可通过 `chrome:inspect` 找到当前页面,找到控件的`xpath`
|
||||
3. 初始化框架并进行相关 API 调用
|
||||
4. 关闭框架,执行用例
|
||||
|
||||
微信小程序:
|
||||
|
||||
```Python
|
||||
# coding=utf-8
|
||||
from fastAutoTest.core.wx.wxEngine import WxDriver
|
||||
import os
|
||||
|
||||
# 进入企鹅医典小程序
|
||||
if __name__ == '__main__':
|
||||
wxDriver = WxDriver()
|
||||
wxDriver.initDriver()
|
||||
# 点击全部疾病
|
||||
wxDriver.clickElementByXpath('/html/body/div[1]/div/div[3]/p')
|
||||
wxDriver.clickFirstElementByText('白内障')
|
||||
wxDriver.returnLastPage()
|
||||
wxDriver.returnLastPage()
|
||||
# 截图
|
||||
dirPath = os.path.split(os.path.realpath(__file__))[0]
|
||||
PIC_SRC = os.path.join(dirPath, 'pic.png')
|
||||
wxDriver.d.screenshot(PIC_SRC)
|
||||
wxDriver.close()
|
||||
```
|
||||
|
||||
1. 搜索小程序,如企鹅医典小程序,进入小程序页面
|
||||
2. 同样找控件的`xpath`,同上操作
|
||||
3. 初始化框架,进行相关 API 调用
|
||||
4. 关闭框架,执行用例
|
||||
|
||||
## 交流群
|
||||
|
||||
交流群,请扫码加入下面群,验证回复 FAutoTest 按照指引进群。
|
||||
|
||||
<img src="docs/images/contact.jpg" width="150"/>
|
||||
|
||||
## 相关链接
|
||||
|
||||
[CONTRIBUTING](docs/CONTRIBUTING.md)
|
||||
|
||||
[IFRAME](docs/IFRAME.md)
|
||||
|
||||
[NOTICES](docs/NOTICES.md)
|
||||
|
||||
[APPENDIX](docs/APPENDIX.md)
|
||||
|
||||
[APIs](docs/APIS.md)
|
||||
|
||||
[QA](docs/QA.md)
|
||||
|
||||
## 贡献代码
|
||||
|
||||
如果你在使用过程中发现 Bug,请通过 Issues 或 Pull Requests 来提交反馈,或者加入交流群来解决。
|
||||
首次参与贡献请阅读:[CONTRIBUTING](docs/CONTRIBUTING.md)
|
||||
|
||||
[腾讯开源激励计划](https://opensource.tencent.com/contribution) 鼓励开发者的参与和贡献,期待你的加入。
|
||||
|
||||
## License
|
||||
|
||||
所有代码采用 [MIT License](http://opensource.org/licenses/MIT) 开源
|
|
@ -0,0 +1,344 @@
|
|||
# APIs
|
||||
|
||||
### 微信H5
|
||||
|
||||
* **def clickElementByXpath(self, xpath, visibleItemXpath =None, duration=50, tapCount=1)**
|
||||
|
||||
> **接口说明:** 点击指定xpath的控件当控件不可见时,会自动滑动到该控件可见时,再进行点击当container为空时,默认滑动点为整个屏幕中间
|
||||
>
|
||||
> **参数说明:** xpath:要点击控件的xpath
|
||||
>
|
||||
> visibleItemXpath:当有container时,传入container中任意一个当前可见item的xpath,之后将目标滑到该可见item的位置
|
||||
>
|
||||
> duration:两次点击的间隔时间
|
||||
>
|
||||
> tapCount:点击次数
|
||||
|
||||
|
||||
|
||||
* **def textElementByXpath(self, xpath, text)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 先获取输入框的焦点,再使用Chrome debug协议的输入api,输入文字内容
|
||||
>
|
||||
> **参数说明:** xpath:输入框的xpath
|
||||
>
|
||||
> text:要输入的文字
|
||||
|
||||
|
||||
|
||||
* **def scrollWindow(self, x, y, xDistance, yDistance, speed=80)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 通过坐标来滑动(屏幕的左上角为(0,0),向下和向右坐标逐渐增大)
|
||||
>
|
||||
> **参数说明:** x: 滑动的起始X点坐标
|
||||
>
|
||||
> y: 滑动的起始Y点坐标
|
||||
>
|
||||
> xDistance: X方向滑动的距离
|
||||
>
|
||||
> yDistance: Y方向滑动的距离
|
||||
>
|
||||
> speed: 滑动的速度
|
||||
|
||||
|
||||
|
||||
* **def getHtml(self, nodeId=1)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 获得指定nodeId的Html代码
|
||||
>
|
||||
> **参数说明:**
|
||||
>
|
||||
> nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
|
||||
|
||||
|
||||
|
||||
- **def closeWindow(self)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 获得getHtml中需要的nodeId,在调用getHtml之前必须先调用这个方法
|
||||
|
||||
|
||||
|
||||
- **def returnLastPage(self)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 返回上一页
|
||||
|
||||
|
||||
|
||||
* **def scrollPickerByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 滑动选择picker的选项
|
||||
>
|
||||
> **参数说明:**
|
||||
>
|
||||
> xpath: 要选择的item
|
||||
|
||||
|
||||
|
||||
* **def getPageHeight(self)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 获取整个page页面的高度
|
||||
|
||||
|
||||
|
||||
* **def getWindowHeight(self)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 获取手机屏幕的高度
|
||||
|
||||
|
||||
|
||||
* **def getWindowWidth(self)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 获取手机屏幕的宽度
|
||||
|
||||
|
||||
|
||||
* **def getElementTextByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 获取目标的text内容
|
||||
>
|
||||
> **参数说明:** xpath:目标的xpath
|
||||
|
||||
|
||||
|
||||
* **def getElementSrcByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 获取目标的src内容
|
||||
>
|
||||
> **参数说明:** xpath:目标的xpath
|
||||
|
||||
|
||||
|
||||
* **def getElementClassNameByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 获取目标的className
|
||||
>
|
||||
> **参数说明:** xpath:目标的xpath
|
||||
|
||||
|
||||
|
||||
* **def getCurrentPageUrl(self)**
|
||||
|
||||
> **接口说明:** 当前页面的url
|
||||
|
||||
|
||||
|
||||
* **def getElementByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
|
||||
>
|
||||
> **参数说明:** xpath:目标的xpath
|
||||
>
|
||||
> 例如:可以使用element.get("attrs")来拿到属性的数据;可以用element.text拿到它的文字;当element中含有列表时,使用for循环读取每一个iteml;更多的lxml的element的操作方法可见http://lxml.de/tutorial.html
|
||||
|
||||
|
||||
|
||||
* **def isElementExist(self, xpath)**
|
||||
|
||||
> **接口说明:** 返回一个boolean,判断该element是否存在
|
||||
>
|
||||
> **参数说明:** xpath: 目标的xpath
|
||||
|
||||
|
||||
|
||||
* **def navigateToPage(self, url)**
|
||||
|
||||
> **接口说明:** 跳转到指定url,在微信版本6.5.13以上不可用
|
||||
>
|
||||
> **参数说明:** url: 要跳转的url
|
||||
|
||||
|
||||
|
||||
* **def executeScript(self, script)**
|
||||
|
||||
> **接口说明:** 手动发送js命令并执行,返回执行结果
|
||||
>
|
||||
> **参数说明:** script:要执行的js指令
|
||||
|
||||
|
||||
|
||||
* **def focusElementByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 调用目标的focus()方法。
|
||||
>
|
||||
> **参数说明:** xpath:目标的xpath
|
||||
|
||||
|
||||
|
||||
* **def scrollToElementByXpath(self, xpath, visibleItemXpath =None, speed=400)**
|
||||
|
||||
> **接口说明:**
|
||||
>
|
||||
> 滑动屏幕,使指定xpath的控件可见默认滑动点为屏幕的中心,且边距为整个屏幕;当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
>
|
||||
> **参数说明:** xpath: 要滑动到屏幕中控件的xpath
|
||||
>
|
||||
> visibleItemXpath: container中当前可见的一个item的xpath;此时滑动位置是该container中间
|
||||
|
||||
|
||||
|
||||
* **def clearInputTextByXpath(self, xpath)**
|
||||
|
||||
> **接口说明:** 清空输入框的文字
|
||||
>
|
||||
> **参数说明:** xpath:input框的xpath
|
||||
|
||||
|
||||
|
||||
* **def getMemoryInfo(self)**
|
||||
|
||||
> **接口说明:** 获得H5进程占用内存信息
|
||||
|
||||
|
||||
|
||||
* **def getCPUInfo(self)**
|
||||
|
||||
> **接口说明:** 获得H5进程占用CPU信息
|
||||
|
||||
|
||||
|
||||
* **def setDebugLogMode(self)**
|
||||
|
||||
> **接口说明:** log信息级别调整设置,默认只显示info级别的消息
|
||||
>
|
||||
> 在initDriver调用后,可以调用此方法来设置显示debug级别的详细的消息
|
||||
|
||||
|
||||
|
||||
* **getElementCoordinateByXpath(self, elementXpath)**
|
||||
|
||||
> **接口说明:** 获得Element的坐标
|
||||
>
|
||||
> **参数说明:** param elementXpath:待获取坐标的元素的xpath
|
||||
>
|
||||
> **返回值:** element相对于整个屏幕的x、y坐标,单位为px
|
||||
|
||||
|
||||
|
||||
* **def longClickElementByXpath(self,xpath, visibleItemXpath=None, duration=2000, tapCount=1)**
|
||||
|
||||
> **接口说明:** 长按点击,默认时间为2s,其他同2.2.1
|
||||
|
||||
|
||||
|
||||
* **def repeatedClickElementByXpath(self,xpath, visibleItemXpath=None, duration=50, tapCount=2)**
|
||||
|
||||
> **接口说明:** 点击多次,默认双击,其他同2.2.1
|
||||
|
||||
|
||||
|
||||
* **getIFrameContextId(self)**
|
||||
|
||||
> **接口说明:** 当body标签中只有一个Iframe时调用,获得contextId
|
||||
|
||||
|
||||
|
||||
* **def getIFrameNodeId(self)**
|
||||
|
||||
> **接口说明:** 当body标签中只有一个Iframe时调用,获得nodeId
|
||||
|
||||
|
||||
|
||||
* **def getAllContext(self)**
|
||||
|
||||
> **接口说明:** 获得当前页面所有的ContextId,域以及FrameId,当有body中有多个跨域Iframe时,可以通过此方法拿到对应Iframe的FrameId
|
||||
|
||||
|
||||
|
||||
* **def getAllIFrameNode(self)**
|
||||
|
||||
> **接口说明:** 获得body标签中所有的IFrameNode
|
||||
|
||||
|
||||
|
||||
* **def getBodyNode(self)**
|
||||
|
||||
> **接口说明:** 获得body中所有node的frameId
|
||||
|
||||
|
||||
|
||||
* **def getIFrameElementCoordinateByXpath(self, elementXpath, iFrameXpath, contextId)**
|
||||
|
||||
> **接口说明:** 获得Element的坐标
|
||||
>
|
||||
> **参数说明:** param elementXpath:待获取坐标的元素的xpath
|
||||
>
|
||||
> iFrameXpath: 外层iFrame的xpath
|
||||
>
|
||||
> contextId: iFrame的ContextId
|
||||
>
|
||||
> **返回值:** element相对于整个屏幕的x、y坐标,单位为px
|
||||
|
||||
|
||||
|
||||
* **def getIFrameHtml(self, nodeId=None)**
|
||||
|
||||
> **接口说明:** 获得指定nodeId页面的Html
|
||||
|
||||
|
||||
|
||||
* **def getIFrameElementByXpath(self, xpath, nodeId)**
|
||||
|
||||
> **接口说明:** nodeId为element所在页面的nodeId,其余同2.2.16
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 小程序
|
||||
|
||||
小程序提供的接口方法与H5类似,可参考H5的接口详细说明
|
||||
|
||||
* **def returnLastPage(self)**
|
||||
|
||||
* **def getPageHeight(self)**
|
||||
|
||||
* **def getWindowWidth(self)**
|
||||
|
||||
* **def isElementExist(self, xpath)**
|
||||
|
||||
* **def getElementTextByXpath(self, xpath)**
|
||||
|
||||
* **def getElementSrcByXpath(self, xpath)**
|
||||
|
||||
* **def textElementByXpath(self, xpath, text, needClick=False)**
|
||||
|
||||
> **参数说明:** needClick: 如果为true,会先对控件进行一次点击,以获得焦点
|
||||
|
||||
* **def clickElementByXpath(self, xpath, containerXpath=None,byUiautomator=False)**
|
||||
|
||||
> **参数说明:** byUiautomator:当传统方式无法点击时,将byUiautomator传入true使用Uiautomator进行点击def getWindowHeight(self):
|
||||
|
||||
* **def scrollWindow(self, x, y, xDistance, yDistance, speed=800)**
|
||||
|
||||
* **def scrollToElementByXpath(self, xpath, containerXpath=None, speed=400)**
|
||||
* **def getDocument(self)**
|
||||
|
||||
* **def getHtml(self, nodeId=1)**
|
||||
|
||||
* **def getElementByXpath(self, xpath)**
|
||||
|
||||
* **def getMemoryInfo(self)**
|
||||
|
||||
* **def getCPUInfo(self)**
|
||||
|
||||
* **def setDebugLogMode(self)**
|
||||
|
||||
* **getElementCoordinateByXpath(self, elementXpath)**
|
|
@ -0,0 +1,38 @@
|
|||
## Appendix
|
||||
|
||||
Chrome调试协议[Chrome Debugging Protocol]([*https://chromedevtools.github.io/devtools-protocol/)是Blink内核支持的调试协议,Chrome以及微信使用的QQ X5浏览器内核均在Blink基础上开发,Chrome F12以及常用的调试工具也是基于Chrome调试协议基础上开发。
|
||||
|
||||
### Chrome 调试协议格式
|
||||
|
||||
* **请求**
|
||||
|
||||
.<img src="images/chromeQuest.png" width="250" style="float:left"/>
|
||||
|
||||
> 1. Id:未使用,对于一次操作来说,请求id=响应id,id只是用来区分当前请求对应的响应
|
||||
> 2. Method:要执行的操作,参见下面的模块域
|
||||
> 3. Params:执行操作传递的参数,具体参见协议文档
|
||||
|
||||
* **响应**
|
||||
|
||||
.<img src="images/chromeResponse.png" width="250" style="float:left"/>
|
||||
|
||||
> 1. Id:与请求id相同
|
||||
> 2. Result:执行结果
|
||||
|
||||
|
||||
|
||||
* **模块域**
|
||||
|
||||
Google把要执行的操作按不同的功能模块域([domains](https://chromedevtools.github.io/debugger-protocol-viewer/1-2/))划分,以在页面上弹框为例:
|
||||
|
||||
Js弹窗操作对应Runtime域中Runtime.evaluate方法
|
||||
|
||||
<img src="images/chromeRuntime.png" width="600" style="float:left"/>
|
||||
|
||||
其中**发送请求**为:
|
||||
|
||||
<img src="images/appendixRuntimeSend.png" width="400" style="float:left"/>.
|
||||
|
||||
**响应:**
|
||||
|
||||
<img src="images/appendixRuntimeResponse.png" width="380" style="float:left"/>.
|
|
@ -0,0 +1,50 @@
|
|||
# Contributing
|
||||
欢迎任意形式的贡献。
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
FAutoTest持续招聘贡献者,即使是在Issues中回答问题或者在群里帮忙解答问题,或者做一些简单的bugfix,也会给FAutoTest带来很大的帮助。
|
||||
|
||||
FAutoTest已开发近半年,在此感谢所有开发者对于FAutoTest的喜欢和支持,希望你能够成为FAutoTest的核心贡献者,加入FAutoTest,共同打造一个更棒更好用的UI自动化测试框架🎉🎉🎉
|
||||
|
||||
## Issues
|
||||
我们通过Github Issues来收集问题和功能相关的支持需求。
|
||||
|
||||
* Bug上报
|
||||
|
||||
如果FAutoTest不工作或者有异常,排除环境问题后,确认是FAutoTest本身问题并且在Issues中搜索未找到相关答案时,欢迎提Issue讨论,同时提问题的时的表述应当尽可能详细准确,以确保您提出的问题是有效的。
|
||||
|
||||
* 新功能支持
|
||||
|
||||
如果你觉得FAutoTest存在不足的地方,或者有更好的idea,欢迎提Issue讨论。
|
||||
|
||||
* 问题探讨
|
||||
|
||||
如果你对FAutoTest存在疑问或者有一些不理解的地方,欢迎提Issue或者进群讨论。
|
||||
|
||||
## Pull Requests
|
||||
我们期待您通过PR(Pull Requests)让FAutoTest变的更加完善。
|
||||
|
||||
### 分支管理
|
||||
仓库一共包含两个分支:
|
||||
|
||||
1. `master` 分支
|
||||
1. **请勿在master分支上提交任何PR。**
|
||||
2. `dev` 分支
|
||||
1. `dev`分支作为稳定的开发分支,经过测试后会在下一个版本合并到`master`分支。
|
||||
2. **PR应该在`dev`分支上提交。**
|
||||
|
||||
### PR流程
|
||||
FAutoTest团队会查看所有的PR,我们会运行一些代码检查和测试,一经测试通过,我们会接受这次PR,但不会立即将代码合并到master分支上,会有一些延迟。
|
||||
|
||||
当您准备PR时,请确保已经完成以下几个步骤:
|
||||
|
||||
1. 将仓库fork下来并且基于`dev`分支创建您的开发分支。
|
||||
2. 如果您更改了APIs请更新代码及文档。
|
||||
3. 在您添加的每一个新文件头部加上版权声明。
|
||||
4. 检查您的代码语法及格式。
|
||||
5. 反复测试。
|
||||
6. 现在,您可以开始在`dev`分支上PR了。
|
||||
|
||||
## 许可证
|
||||
通过为FAutoTest做出贡献,代表您同意将其版权归为FAutoTest所有,FAutoTest的开源协议为 [MIT License](http://opensource.org/licenses/MIT)
|
|
@ -0,0 +1,22 @@
|
|||
# 打开微信Debug模式
|
||||
|
||||
1. 点击任意一个聊天窗口
|
||||
|
||||
2. 发送消息 **debugtbs.qq.com**,并点击进入
|
||||
|
||||
<img src='images/debug1.jpeg' width="300"/>
|
||||
|
||||
3. 进入下图界面,点击安装线上内核
|
||||
|
||||
<img src='images/debug2.jpeg' width="300"/>
|
||||
|
||||
4. 安装成功后,继续发送消息 **debugx5.qq.com** ,并点击进入
|
||||
|
||||
5. 进入信息标签,勾选如下图两个选项
|
||||
|
||||
<img src ='images/debug3.jpeg' width="300"/>
|
||||
|
||||
6. 之后,打开任意h5页面,在**chrome:inspect**中可以看到该页面的信息,说明开启成功。
|
||||
|
||||
<img src='images/h5.png' width="300"/>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# 如何操作跨域的IFrame页面
|
||||
- **https://open.captcha.qq.com/online.html 以该页面为例**
|
||||
|
||||
1. 当点击体验验证码按钮后,如图所示:
|
||||
* <img src="images/iframe1.png"/>
|
||||
|
||||
|
||||
|
||||
2. 待操作页面在一个跨域的IFrame中,要操作这个页面中的元素,必须先获取到该页面的**contextId** 。
|
||||
|
||||
* 首先需要获取到IFrame的**frameId** 。
|
||||
* 调用方法,获取所有元素的Node信息。如图:
|
||||
* <img src="images/iframe2.png"/>
|
||||
* nodeLIst结果如图:
|
||||
* <img src="images/iframe3.png"/>
|
||||
* 通过HTML页面中的标签,对应获取到需要操作的IFrame页面的**frameId** 以及**nodeId** :
|
||||
* <img src="images/iframe4.png"/>
|
||||
* 之后再调用下图方法:
|
||||
* <img src="images/iframe5.png"/>
|
||||
* 根据之前找到的**frameId** ,获取**contextId**
|
||||
* <img src="images/iframe6.png"/>
|
||||
* 对比上图,**contenxtId** 为7。
|
||||
|
||||
3. 获取到id之后,可以调用方法进行点击,获取属性等,根据方法不同,传入**nodeId** 以及**ContextId**
|
||||
|
||||
* <img src="images/iframe7.png"/>
|
||||
* <img src="images/iframe8.png"/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# 注意事项
|
||||
|
||||
### 1. 设备连接
|
||||
|
||||
当连接有多个设备时,需要先传入设备号。
|
||||
|
||||
H5页面:` h5Driver = H5Driver(deviceId)`
|
||||
|
||||
小程序:`wxDriver = WxDriver(deviceId)`
|
||||
|
||||
### 2. 初始化和结束框架
|
||||
|
||||
进入H5页面之后再调用 `initDriver()`
|
||||
|
||||
使用前在小程序首屏时调用`initDriver()`
|
||||
|
||||
在测试完成后需调用`driver.close()`方法结束框架
|
||||
|
||||
### 3. 环境一致性
|
||||
|
||||
最好在启动H5页面/小程序前先杀掉微信后台进程,保证环境一致
|
||||
|
||||
初始化小程序框架时,要确保微信只开启过这一个小程序;如果之前开启过其他小程序,需要使用`adb shell am force-stop com.tencent.mm`命令杀掉微信进程,再开启小程序进行测试
|
||||
|
||||
### 4. 控件查找
|
||||
|
||||
如何找到要点击的控件的xpath:通过`chrome:inspect` 找到当前页面,可以找到要点击控件的xpath(小程序一般是在当前页面的inspect的第二个)
|
||||
|
||||
### 5. 与Native页面组合操作
|
||||
|
||||
框架内部已经封装了UiAutomator来进行native的操作:可以通过`driver.d`来执行,可执行的操作与java的UiAutomator类似,具体使用方法可以见[uiautomator](*https://github.com/xiaocong/uiautomator*)
|
|
@ -0,0 +1,42 @@
|
|||
## Q&A
|
||||
|
||||
1. pip安装过程中,一直出现timeout,安装失败
|
||||
> - 请设置是否设置代理,若设置了请检查代理设置是否正确,若仍无法解决,可手动下载无法安装的库,然后进行本地安装
|
||||
|
||||
2. pip安装过程中,出现Command “python setup.py egg_info failed with error code 1”
|
||||
> - 安装pip install setuptools_scm ; 安装 pip install pytest-runner
|
||||
|
||||
3. 小程序框架初始化一直报错
|
||||
|
||||
> * 有可能在开启小程序之前开启过别的小程序。需要使用adb shell am force-stop com.tencent.mm命令杀掉微信进程
|
||||
|
||||
4. 封装的UIAutomator无法正常使用,一直报告prc错误
|
||||
|
||||
> * 因为手机环境问题,UIAutomator没有正常安装,执行命令:
|
||||
>
|
||||
> * ```
|
||||
> adb shell pm uninstall com.github.uiautomator
|
||||
> adb shell pm uninstall com.github.uiautomator.test
|
||||
> adb install app-uiautomator.apk
|
||||
> adb install app-uiautomator-test.apk
|
||||
> adb push bundle.jar /data/local/tmp/
|
||||
> adb push uiautomator-stub.jar /data/local/tmp/
|
||||
> ```
|
||||
>
|
||||
> 详情可见 [uiautomator]([*https://github.com/xiaocong/uiautomator/issues/172*](https://github.com/xiaocong/uiautomator/issues/172) )
|
||||
|
||||
5. 拖拽等操作怎么进行
|
||||
|
||||
> 通过getElementCoordinateByXpath获得坐标。然后调用driver.d.drag() UIAutomator的方法来进行操作。
|
||||
|
||||
6. 如果body中有多个跨域的Iframe怎么办
|
||||
|
||||
> 调用getAllContext、getAllIFrameNode、getBodyNode方法,通过返回值来获取需要的contextId以及NodeId
|
||||
> [Iframe操作指引](IFRAME.md)
|
||||
|
||||
7. 为什么部分机型小程序无法进入debug模式(在**chrome:inspect**中也没有小程序页面显示)
|
||||
|
||||
> 最新版本微信会根据机型来使用不同的浏览器内核。如果没有使用QQ浏览器X5内核,则无法进入调试模式。
|
||||
> 可以安装提供的6.6.3版本的微信,通过微信降级来解决。
|
||||
> [6.6.3版本微信下载](assert/weixin663android1260.apk)
|
||||
> 降级微信后,需要重新开启微信debug模式,见[开启微信debug模式](DEBUG.md)
|
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 133 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 258 KiB |
After Width: | Height: | Size: 272 KiB |
After Width: | Height: | Size: 220 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 204 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 348 KiB |
After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import string
|
||||
|
||||
from jsonConcat import JsonConcat
|
||||
from fastAutoTest.core.h5 import h5CommandManager, h5UserAPI
|
||||
from fastAutoTest.core.wx import wxCommandManager, wxUserAPI
|
||||
|
||||
|
||||
class CommandProcessor(object):
|
||||
def __init__(self, managerType):
|
||||
if managerType == 'h5':
|
||||
self.manager = h5CommandManager.H5CommandManager()
|
||||
self.userAPI = h5UserAPI
|
||||
self.concat = JsonConcat('h5')
|
||||
else:
|
||||
self.manager = wxCommandManager.WxCommandManager()
|
||||
self.userAPI = wxUserAPI
|
||||
self.concat = JsonConcat('wx')
|
||||
|
||||
'''
|
||||
通过id查找,则传入{"id":id},
|
||||
通过name查找,传入{"name":"name","index",index}
|
||||
通过Xpath查找,传入{"xpath":"//*[@id=\"dctlist\""}
|
||||
'''
|
||||
|
||||
def doCommandWithElement(self, byType, actionType, text=None, **domType):
|
||||
|
||||
jsActionType = self.manager.getJsAction(actionType, None)
|
||||
|
||||
if jsActionType is not None:
|
||||
|
||||
# 找到控件dom
|
||||
domTemplate = self.manager.getElement(byType)
|
||||
domTemplateType = string.Template(domTemplate)
|
||||
domResult = domTemplateType.substitute(**domType)
|
||||
# 使用xpath找控件,需要先转义成\\\",在json中会转义成\",在js中执行"
|
||||
if byType == self.userAPI.ByType.XPATH:
|
||||
domResult = domResult.replace('"', '\\\"')
|
||||
# 组装json命令
|
||||
if actionType == self.userAPI.ActionType.TEXT:
|
||||
if text is None:
|
||||
raise TypeError('请输入要设置的text值')
|
||||
jsActionTemplate = string.Template(jsActionType)
|
||||
jsActionResult = jsActionTemplate.substitute({"text": text})
|
||||
else:
|
||||
jsActionResult = jsActionType
|
||||
jsCommand = domResult + jsActionResult
|
||||
params = {"expression": jsCommand}
|
||||
jsonResult = self.concat.concat(actionType, **params)
|
||||
return jsonResult
|
||||
else:
|
||||
raise TypeError('ActionType错误')
|
||||
|
||||
'''
|
||||
不需要找到控件的command,比如滑动,获得html,获得node等。参数根据不同的actionType传入。
|
||||
比如当actionType为SCROLL,参数为
|
||||
{
|
||||
"x": "x", "y": "y","xDistance": "xDistance", "yDistance": "yDistance"
|
||||
}
|
||||
也可以是CLOSE_WINDOW等不需要参数的actionType
|
||||
'''
|
||||
|
||||
def doCommandWithoutElement(self, actionType, **kw):
|
||||
kwResult = self.manager.getExpression(actionType, None)
|
||||
if kwResult is not None:
|
||||
# getValue的时候会传入value值
|
||||
if kw is not None:
|
||||
paramsCat = string.Template(kwResult)
|
||||
kwResult = paramsCat.substitute(**kw)
|
||||
params = dict()
|
||||
params["expression"] = kwResult
|
||||
jsonResult = self.concat.concat(actionType, **params)
|
||||
else:
|
||||
jsonResult = self.concat.concat(actionType, **kw)
|
||||
return jsonResult
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import string
|
||||
|
||||
from fastAutoTest.core.h5 import h5CommandManager
|
||||
from fastAutoTest.core.wx import wxCommandManager
|
||||
|
||||
|
||||
class JsonConcat(object):
|
||||
def __init__(self, managerType):
|
||||
if managerType == "h5":
|
||||
self.manager = h5CommandManager.H5CommandManager()
|
||||
else:
|
||||
self.manager = wxCommandManager.WxCommandManager()
|
||||
|
||||
def concat(self, action_type, **params):
|
||||
method = self.manager.getMethod(action_type, None)
|
||||
if len(params) != 0:
|
||||
paramsTemplate = self.manager.getParams(method)
|
||||
paramsCat = string.Template(paramsTemplate)
|
||||
paramsResult = paramsCat.substitute(**params)
|
||||
paramsResult = json.loads(paramsResult)
|
||||
|
||||
# print json.dumps(params_result)
|
||||
else:
|
||||
# 有getDocument这些不需要参数的情况
|
||||
paramsResult = "{}"
|
||||
result = dict()
|
||||
result['method'] = method
|
||||
result['params'] = paramsResult
|
||||
jsonResult = json.dumps(result)
|
||||
return jsonResult
|
|
@ -0,0 +1,112 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
"""
|
||||
设备连接类错误信息
|
||||
"""
|
||||
ERROR_CODE_DEVICE_NOT_CONNECT = 1000 # 设备没有连上
|
||||
ERROR_CODE_MULTIPLE_DEVICE = 1001 # 有多个设备连接,但是使用时并没有指定具体设备
|
||||
ERROR_CODE_NOT_CONFIG_ENV = 1002 # 没有配置环境变量
|
||||
|
||||
"""
|
||||
获取debug url相关错误
|
||||
"""
|
||||
ERROR_CODE_MULTIPLE_URL = 2000 # 多个debug URL
|
||||
ERROR_CODE_NOT_ENABLE_DEBUG_MODE = 2001 # 没有打开调试模式、
|
||||
ERROR_CODE_NOT_ENTER_H5 = 2002 # 打开了调试模式, 但是当前并没有进入H5页面
|
||||
ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS = 2003 # 找不到微信Tools进程
|
||||
ERROR_CODE_CONFIG_PROXY = 2004 # 无法获取debug url,检查是否配置了代理
|
||||
ERROR_CODE_NOT_ENTER_XCX = 2005 # 未在小程序首屏进行初始化
|
||||
ERROR_CODE_NOT_GET_XCX_PAGE_INFO = 2006 # 获取小程序页面特征失败
|
||||
ERROR_CODE_GET_PID_WRONG = 2007 # 检测到小程序进程,获取PID失败
|
||||
|
||||
"""
|
||||
协议相关操作错误
|
||||
"""
|
||||
ERROR_CODE_BAD_REQUEST = 3000
|
||||
ERROR_CODE_REQUEST_EXCEPTION = 3001
|
||||
ERROR_CODE_REQUEST_NOT_MATCH_RESPONSE = 3002
|
||||
|
||||
"""
|
||||
websocket链接相关错误
|
||||
"""
|
||||
ERROR_CODE_CONNECT_CLOSED = 4000
|
||||
|
||||
"""
|
||||
运行时错误
|
||||
"""
|
||||
ERROR_CODE_GETCOORDINATE = 5000 # 获取Element坐标失败,该Element不存在
|
||||
ERROR_CODE_SETUP_FRAME_PAGE = 5001 # Body标签中的IFrame页面不存在或还未加载
|
||||
|
||||
"""
|
||||
未知错误
|
||||
"""
|
||||
ERROR_CODE_UNKNOWN = -999999 # 未知错误
|
||||
|
||||
_ERROR_CODE_SET = [
|
||||
ERROR_CODE_NOT_CONFIG_ENV,
|
||||
ERROR_CODE_DEVICE_NOT_CONNECT,
|
||||
ERROR_CODE_MULTIPLE_DEVICE,
|
||||
ERROR_CODE_MULTIPLE_URL,
|
||||
ERROR_CODE_NOT_ENABLE_DEBUG_MODE,
|
||||
ERROR_CODE_NOT_ENTER_H5,
|
||||
ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS,
|
||||
ERROR_CODE_CONFIG_PROXY,
|
||||
ERROR_CODE_BAD_REQUEST,
|
||||
ERROR_CODE_REQUEST_EXCEPTION,
|
||||
ERROR_CODE_REQUEST_NOT_MATCH_RESPONSE,
|
||||
ERROR_CODE_CONNECT_CLOSED,
|
||||
ERROR_CODE_UNKNOWN,
|
||||
ERROR_CODE_NOT_ENTER_XCX,
|
||||
ERROR_CODE_NOT_GET_XCX_PAGE_INFO,
|
||||
ERROR_CODE_GET_PID_WRONG,
|
||||
ERROR_CODE_GETCOORDINATE,
|
||||
ERROR_CODE_SETUP_FRAME_PAGE
|
||||
]
|
||||
|
||||
_ERROR_MSG_MAPPING = {
|
||||
ERROR_CODE_NOT_CONFIG_ENV: "执行adb命令失败,请检查是否配置系统环境变量",
|
||||
ERROR_CODE_DEVICE_NOT_CONNECT: "没有设备连上,请用adb device确认是否有设备连接到PC",
|
||||
ERROR_CODE_MULTIPLE_DEVICE: "当前有多个设备连接到PC,请创建H5Driver时指定要操作的设备",
|
||||
ERROR_CODE_MULTIPLE_URL: "检测到多个debug url",
|
||||
ERROR_CODE_NOT_ENABLE_DEBUG_MODE: "请在微信端打开H5调试模式,或者后台杀死微信进程后重试",
|
||||
ERROR_CODE_NOT_ENTER_H5: "在执行脚本前,先进入H5页面",
|
||||
ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS: "找不到微信Tools进程",
|
||||
ERROR_CODE_CONFIG_PROXY: "无法获取debug url,并检查是否配置了代理,是否已经建立了websocket连接未关闭",
|
||||
ERROR_CODE_NOT_ENTER_XCX: "获取小程序pid失败,请检查是否在小程序首屏进行初始化",
|
||||
ERROR_CODE_NOT_GET_XCX_PAGE_INFO: "获取小程序页面特征失败",
|
||||
ERROR_CODE_BAD_REQUEST: "操作错误,请检查",
|
||||
ERROR_CODE_REQUEST_EXCEPTION: "操作发生异常",
|
||||
ERROR_CODE_REQUEST_NOT_MATCH_RESPONSE: "请求和响应不匹配",
|
||||
ERROR_CODE_CONNECT_CLOSED: "websocket链接已关闭",
|
||||
ERROR_CODE_GET_PID_WRONG: "检测到小程序进程,获取PID失败",
|
||||
ERROR_CODE_UNKNOWN: "未知错误",
|
||||
ERROR_CODE_GETCOORDINATE: "获取Element坐标失败,该Element不存在",
|
||||
ERROR_CODE_SETUP_FRAME_PAGE: "Body标签中的IFrame页面不存在或还未加载"
|
||||
|
||||
}
|
||||
|
||||
|
||||
class ErrorMsgManager(object):
|
||||
_instance = None
|
||||
|
||||
"""
|
||||
单例模式
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(ErrorMsgManager, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
return cls._instance
|
||||
|
||||
def errorCodeToString(self, errorCode):
|
||||
if errorCode not in _ERROR_CODE_SET:
|
||||
errorCode = ERROR_CODE_UNKNOWN
|
||||
|
||||
return _ERROR_MSG_MAPPING[errorCode]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
errorMgr = ErrorMsgManager()
|
||||
print(errorMgr.errorCodeToString(0))
|
|
@ -0,0 +1,184 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
from websocket import WebSocketConnectionClosedException
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import ErrorMsgManager, ERROR_CODE_CONNECT_CLOSED, \
|
||||
ERROR_CODE_BAD_REQUEST
|
||||
from fastAutoTest.core.common.network.websocketdatatransfer import DataTransferCallback
|
||||
from fastAutoTest.utils.atomicinteger import AtomicInteger
|
||||
from fastAutoTest.utils.constant import WAIT_REFLESH_60_SECOND, WAIT_REFLESH_40_SECOND, WAIT_REFLESH_2_SECOND, \
|
||||
SEND_DATA_ATTEMPT_TOTAL_NUM, WAIT_REFLESH_1_SECOND, WXDRIVER, QQDRIVER
|
||||
from fastAutoTest.utils.logger import Log
|
||||
|
||||
__all__ = [
|
||||
'ShortLiveWebSocket'
|
||||
]
|
||||
|
||||
|
||||
class _NetWorkRequset(object):
|
||||
def __init__(self, id, requestData):
|
||||
self._id = id
|
||||
self._requestData = requestData
|
||||
self._response = []
|
||||
self._exception = None
|
||||
|
||||
def toSendJsonString(self):
|
||||
lastBraceIndex = self._requestData.rfind('}')
|
||||
sendJson = self._requestData[:lastBraceIndex] \
|
||||
+ ", \"id\"" + ":" + str(self._id) \
|
||||
+ self._requestData[lastBraceIndex:]
|
||||
return sendJson
|
||||
|
||||
def getResponse(self):
|
||||
return self._response
|
||||
|
||||
def addResponse(self, responseStr):
|
||||
response = json.loads(responseStr)
|
||||
self._checkSuccessOrThrow(response)
|
||||
self._response.append(response)
|
||||
|
||||
def getRequestData(self):
|
||||
return self._requestData
|
||||
|
||||
def _checkSuccessOrThrow(self, response):
|
||||
responseId = response.get('id')
|
||||
if responseId is not None:
|
||||
if self._id != int(responseId):
|
||||
raise RuntimeError("error id")
|
||||
del response['id']
|
||||
|
||||
def getException(self):
|
||||
return self._exception
|
||||
|
||||
def setException(self, exception):
|
||||
self._exception = exception
|
||||
|
||||
|
||||
class ShortLiveWebSocket(DataTransferCallback):
|
||||
def __init__(self, webSocketDataTransfer, executor, driver):
|
||||
super(ShortLiveWebSocket, self).__init__()
|
||||
self._webSocketDataTransfer = webSocketDataTransfer
|
||||
self._executor = executor
|
||||
self._webSocketDataTransfer.registerCallback(self)
|
||||
|
||||
self._readWriteSyncEvent = threading.Event()
|
||||
self._connectSyncEvent = threading.Event()
|
||||
self._id = AtomicInteger()
|
||||
self._currentRequest = None
|
||||
|
||||
self._quit = False
|
||||
|
||||
self._retryEvent = threading.Event()
|
||||
self.driver = driver
|
||||
self.logger = Log().getLogger()
|
||||
|
||||
def setUrl(self, url):
|
||||
self._webSocketDataTransfer.setUrl(url)
|
||||
|
||||
def isConnected(self):
|
||||
return self._webSocketDataTransfer.isConnected()
|
||||
|
||||
def connect(self):
|
||||
self._webSocketDataTransfer.connect()
|
||||
self._connectSyncEvent.set()
|
||||
|
||||
def disconnect(self):
|
||||
self._webSocketDataTransfer.disconnect()
|
||||
self._connectSyncEvent.clear()
|
||||
|
||||
def receive(self):
|
||||
while not self.isQuit():
|
||||
try:
|
||||
self._waitForConnectOrThrow()
|
||||
self._webSocketDataTransfer.receive()
|
||||
|
||||
except WebSocketConnectionClosedException:
|
||||
if self.isQuit():
|
||||
print 'already quit'
|
||||
else:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def send(self, data, timeout=int(1 * 60)):
|
||||
self._waitForConnectOrThrow()
|
||||
# 微信小程序和QQ公粽号都需要切换页面
|
||||
if self.driver.getDriverType() == WXDRIVER or self.driver.getDriverType == QQDRIVER:
|
||||
# 只有点击事件才会导致页面的切换
|
||||
if 'x=Math.round((left+right)/2)' in data:
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
if self.driver.needSwitchNextPage():
|
||||
self.driver.switchToNextPage()
|
||||
|
||||
self._currentRequest = _NetWorkRequset(self._id.getAndIncrement(), data)
|
||||
currentRequestToJsonStr = self._currentRequest.toSendJsonString()
|
||||
self.logger.debug(' ---> ' + currentRequestToJsonStr)
|
||||
|
||||
# scroll操作需要滑动到位置才会有返回,如果是scroll操作则等待,防止超时退出
|
||||
if 'synthesizeScrollGesture' in data:
|
||||
self._webSocketDataTransfer.send(currentRequestToJsonStr)
|
||||
self._retryEvent.wait(WAIT_REFLESH_40_SECOND)
|
||||
else:
|
||||
for num in range(0, SEND_DATA_ATTEMPT_TOTAL_NUM):
|
||||
self.logger.debug(" ---> attempt num: " + str(num))
|
||||
|
||||
if num != 3 and num != 5:
|
||||
self._webSocketDataTransfer.send(currentRequestToJsonStr)
|
||||
self._retryEvent.wait(3)
|
||||
else:
|
||||
self.driver.switchToNextPage()
|
||||
time.sleep(WAIT_REFLESH_2_SECOND)
|
||||
self.logger.debug('switch when request: ' + currentRequestToJsonStr)
|
||||
self._webSocketDataTransfer.send(currentRequestToJsonStr)
|
||||
self._retryEvent.wait(WAIT_REFLESH_2_SECOND)
|
||||
|
||||
if self._readWriteSyncEvent.isSet():
|
||||
break
|
||||
|
||||
self._readWriteSyncEvent.wait(timeout=timeout)
|
||||
self._readWriteSyncEvent.clear()
|
||||
self._retryEvent.clear()
|
||||
|
||||
self._checkReturnOrThrow(self._currentRequest)
|
||||
|
||||
return self._currentRequest
|
||||
|
||||
def quit(self):
|
||||
self._quit = True
|
||||
self.disconnect()
|
||||
|
||||
def isQuit(self):
|
||||
return self._quit
|
||||
|
||||
def _checkReturnOrThrow(self, netWorkRequest):
|
||||
needThrown = (netWorkRequest.getResponse() is None and
|
||||
netWorkRequest.getException() is None)
|
||||
if needThrown:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_BAD_REQUEST)
|
||||
raise RuntimeError("%s, request data {%s}" % (errorMsg, netWorkRequest.getRequestData()))
|
||||
|
||||
def _waitForConnectOrThrow(self):
|
||||
if self._webSocketDataTransfer.isConnected():
|
||||
return
|
||||
|
||||
self._connectSyncEvent.wait(WAIT_REFLESH_60_SECOND)
|
||||
|
||||
if not self._webSocketDataTransfer.isConnected():
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_CONNECT_CLOSED)
|
||||
raise RuntimeError("connect %s timeout, %s" % (self._webSocketDataTransfer.getUrl(), errorMsg))
|
||||
|
||||
def onMessage(self, message):
|
||||
if self._currentRequest is None:
|
||||
raise RuntimeError("current request is None")
|
||||
if message is not None:
|
||||
self._currentRequest.addResponse(message)
|
||||
# 只有当response带有id,所有response才接收完
|
||||
if json.loads(message).get('id') is not None:
|
||||
self._retryEvent.set()
|
||||
self._readWriteSyncEvent.set()
|
||||
self.logger.debug(' ---> ' + message)
|
||||
else:
|
||||
self.logger.debug(' --->' + 'message is None')
|
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import threading
|
||||
|
||||
from websocket import create_connection
|
||||
|
||||
__all__ = [
|
||||
"WebSocketDataTransfer",
|
||||
"DataTransferCallback"
|
||||
]
|
||||
|
||||
|
||||
class DataTransferCallback(object):
|
||||
def onMessage(self, message):
|
||||
pass
|
||||
|
||||
|
||||
class WebSocketDataTransfer(object):
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
self._callbacks = []
|
||||
self._webSocket = None
|
||||
self._isConnected = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def setUrl(self, url):
|
||||
self._url = url
|
||||
|
||||
def getUrl(self):
|
||||
return self._url
|
||||
|
||||
def connect(self):
|
||||
if not self._isConnected:
|
||||
self._webSocket = create_connection(url=self._url)
|
||||
self._isConnected = True
|
||||
|
||||
def disconnect(self):
|
||||
self._webSocket.close()
|
||||
self._isConnected = False
|
||||
|
||||
def send(self, data):
|
||||
self._webSocket.send(data)
|
||||
|
||||
def receive(self):
|
||||
message = self._webSocket.recv()
|
||||
self._onMessage(message)
|
||||
|
||||
def isConnected(self):
|
||||
return self._isConnected
|
||||
|
||||
def registerCallback(self, callback):
|
||||
if not isinstance(callback, DataTransferCallback):
|
||||
raise RuntimeError("call back must be DataTransferCallback sub class")
|
||||
|
||||
with self._lock:
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def unregisterCallback(self, callback):
|
||||
if not isinstance(callback, DataTransferCallback):
|
||||
raise RuntimeError("call back must be DataTransferCallback sub class")
|
||||
with self._lock:
|
||||
self._callbacks.remove(callback)
|
||||
|
||||
def _onMessage(self, message):
|
||||
with self._lock:
|
||||
for callback in self._callbacks:
|
||||
callback.onMessage(message)
|
||||
|
||||
|
||||
class TestCallback(DataTransferCallback):
|
||||
def onMessage(self, message):
|
||||
print("onMessage --> " + message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,119 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastAutoTest.core.h5.h5UserAPI import ActionType
|
||||
from fastAutoTest.core.h5.h5UserAPI import ByType
|
||||
|
||||
|
||||
class H5CommandManager(object):
|
||||
# 使用$$可以作为格式化时的转义
|
||||
_elementMap = {
|
||||
ByType.ID: "$$('#$id')[0]",
|
||||
ByType.NAME: "$$('.$name')[$index]",
|
||||
ByType.XPATH: "var xpath ='$xpath';"
|
||||
"xpath_obj = document.evaluate(xpath,document,null, XPathResult.ANY_TYPE, null);"
|
||||
"var button = xpath_obj.iterateNext()"
|
||||
}
|
||||
|
||||
_methodMap = {
|
||||
ActionType.GET_DOCUMENT: "DOM.getDocument",
|
||||
ActionType.GET_HTML: "DOM.getOuterHTML",
|
||||
ActionType.SCROLL: "Input.synthesizeScrollGesture",
|
||||
ActionType.CLICK: "Input.synthesizeTapGesture",
|
||||
ActionType.FOCUS: "Runtime.evaluate",
|
||||
ActionType.RETURN_LAST_PAGE: "Runtime.evaluate",
|
||||
ActionType.CLOSE_WINDOW: "Runtime.evaluate",
|
||||
ActionType.GET_PICKER_RECT: "Runtime.evaluate",
|
||||
ActionType.GET_JS_VALUE: "Runtime.evaluate",
|
||||
ActionType.GET_ELEMENT_TEXT: "Runtime.evaluate",
|
||||
ActionType.GET_ELEMENT_SRC: "Runtime.evaluate",
|
||||
ActionType.GET_ELEMENT_CLASS_NAME: "Runtime.evaluate",
|
||||
ActionType.IS_ELEMENT_EXIST: "Runtime.evaluate",
|
||||
ActionType.NAVIGATE_PAGE: "Page.navigate",
|
||||
ActionType.GET_PAGE_HEIGHT: "Runtime.evaluate",
|
||||
ActionType.EXECUTE_SCRIPT: "Runtime.evaluate",
|
||||
ActionType.TEXT: "Input.dispatchKeyEvent",
|
||||
ActionType.GET_ELEMENT_RECT: "Runtime.evaluate",
|
||||
ActionType.GET_WINDOW_HEIGHT: "Runtime.evaluate",
|
||||
ActionType.GET_WINDOW_WIDTH: "Runtime.evaluate",
|
||||
ActionType.CLEAR_INPUT_TEXT: "Runtime.evaluate",
|
||||
ActionType.GET_PAGE_URL: "Runtime.evaluate",
|
||||
ActionType.GET_ALL_CONTEXT: "Runtime.enable",
|
||||
ActionType.GET_BODY_NODE: "DOM.requestChildNodes",
|
||||
ActionType.REQUEST_CHILD_NODES: "DOM.requestChildNodes"
|
||||
|
||||
}
|
||||
|
||||
# doCommandWithElement中执行的参数
|
||||
_jsActionMap = {
|
||||
ActionType.FOCUS: ";button.focus();",
|
||||
ActionType.GET_PICKER_RECT: ";var parent = button.parentElement;"
|
||||
"var pparent = parent.parentElement;"
|
||||
"var count = parent.childElementCount;"
|
||||
"for(var i = 0;i<count;i++)"
|
||||
"{if(button == parent.childNodes[i])"
|
||||
"{var index = i}};"
|
||||
"bound = parent.getBoundingClientRect();"
|
||||
"height = bound.height;"
|
||||
"per_item_height = height/count;"
|
||||
"dex = per_item_height * index;"
|
||||
"start_scroll_x = bound.left + 5;"
|
||||
"start_scroll_y = pparent.getBoundingClientRect().top+20;",
|
||||
ActionType.GET_ELEMENT_TEXT: ";button.innerText;",
|
||||
ActionType.GET_ELEMENT_SRC: ";button.src;",
|
||||
ActionType.GET_ELEMENT_CLASS_NAME: ";button.className;",
|
||||
ActionType.IS_ELEMENT_EXIST: ";button;",
|
||||
ActionType.GET_ELEMENT_RECT: ";left=Math.round(button.getBoundingClientRect().left);"
|
||||
"right=Math.round(button.getBoundingClientRect().right);"
|
||||
"bottom=Math.round(button.getBoundingClientRect().bottom);"
|
||||
"topp=Math.round(button.getBoundingClientRect().top);"
|
||||
"x=Math.round((left+right)/2);"
|
||||
"y=Math.round((topp+bottom)/2);",
|
||||
ActionType.CLEAR_INPUT_TEXT: ";button.value=''",
|
||||
}
|
||||
|
||||
# doCommandWithoutElement 中执行的参数
|
||||
_expressionMap = {
|
||||
ActionType.RETURN_LAST_PAGE: 'history.back()',
|
||||
ActionType.CLOSE_WINDOW: "WeixinJSBridge.call('closeWindow')",
|
||||
ActionType.GET_JS_VALUE: '$value',
|
||||
ActionType.GET_PAGE_HEIGHT: 'document.body.scrollHeight',
|
||||
ActionType.EXECUTE_SCRIPT: '$script',
|
||||
ActionType.GET_WINDOW_HEIGHT: 'document.documentElement.clientHeight',
|
||||
ActionType.GET_WINDOW_WIDTH: "document.documentElement.clientWidth",
|
||||
ActionType.GET_PAGE_URL: "window.location.href"
|
||||
}
|
||||
|
||||
# string.Template
|
||||
# 用在jsonConcat的格式化中
|
||||
_paramsMap = {
|
||||
"Runtime.evaluate": '{"expression": "$expression"}',
|
||||
"DOM.getDocument": "{''}",
|
||||
"DOM.getOuterHTML": '{"nodeId": $nodeId}',
|
||||
"Input.synthesizeScrollGesture":
|
||||
'{"type": "mouseWheel", "x": $x, "y": $y,"xDistance": $xDistance, "yDistance": $yDistance,"speed":$speed}',
|
||||
"Page.navigate": '{"url":"$url"}',
|
||||
"Input.dispatchKeyEvent": '{"type":"$type","text":"$text"}',
|
||||
"Input.synthesizeTapGesture": '{"x":$x,"y":$y,"duration":$duration,"tapCount":$tapCount}',
|
||||
"Runtime.enable": "{''}",
|
||||
"DOM.requestChildNodes": '{"nodeId":$nodeId,"pierce":true,"depth":-1}'
|
||||
|
||||
}
|
||||
|
||||
def getElement(self, actionType, default=None):
|
||||
return self._elementMap.get(actionType, default)
|
||||
|
||||
def getJsAction(self, actionType, default=None):
|
||||
return self._jsActionMap.get(actionType, default)
|
||||
|
||||
def getMethod(self, actionType, default=None):
|
||||
return self._methodMap.get(actionType, default)
|
||||
|
||||
def getParams(self, actionType, default=None):
|
||||
return self._paramsMap.get(actionType, default)
|
||||
|
||||
def getExpression(self, actionType, default=None):
|
||||
return self._expressionMap.get(actionType, default)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,711 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
import uiautomator
|
||||
import fastAutoTest.utils.commandHelper as commandHelper
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from fastAutoTest.core.common.network.shortLiveWebSocket import ShortLiveWebSocket
|
||||
from fastAutoTest.core.common.network.websocketdatatransfer import WebSocketDataTransfer
|
||||
|
||||
from fastAutoTest.core.h5.h5WebSocketDebugUrlFetcher import H5WebSocketDebugUrlFetcher
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import ErrorMsgManager, ERROR_CODE_DEVICE_NOT_CONNECT, \
|
||||
ERROR_CODE_MULTIPLE_DEVICE, ERROR_CODE_GETCOORDINATE, ERROR_CODE_SETUP_FRAME_PAGE
|
||||
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.constant import WAIT_REFLESH_05_SECOND, WAIT_REFLESH_2_SECOND, H5DRIVER
|
||||
|
||||
from fastAutoTest.utils.singlethreadexecutor import SingleThreadExecutor
|
||||
|
||||
from fastAutoTest.core.h5.h5PageOperator import H5PageOperator
|
||||
|
||||
from fastAutoTest.utils.vmhook import VMShutDownHandler, UncaughtExceptionHandler
|
||||
from fastAutoTest.utils import constant
|
||||
from fastAutoTest.utils.logger import Log
|
||||
|
||||
import sys
|
||||
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
__all__ = [
|
||||
"H5Driver"
|
||||
]
|
||||
|
||||
|
||||
class H5Driver(object):
|
||||
def __init__(self, device=None):
|
||||
self._device = device
|
||||
self._urlFetcher = None
|
||||
self._webSocketDataTransfer = None
|
||||
self._vmShutdownHandler = VMShutDownHandler()
|
||||
self._networkHandler = None
|
||||
|
||||
self._pageOperator = H5PageOperator()
|
||||
self.d = uiautomator.Device(self._device)
|
||||
self._hasInit = False
|
||||
|
||||
self.html = None
|
||||
self.bodyNode = None
|
||||
|
||||
def initDriver(self):
|
||||
if self._hasInit:
|
||||
return
|
||||
self._executor = SingleThreadExecutor()
|
||||
self._vmShutdownHandler.registerToVM()
|
||||
UncaughtExceptionHandler.init()
|
||||
UncaughtExceptionHandler.registerUncaughtExceptionCallback(self._onUncaughtException)
|
||||
|
||||
self._selectDevice()
|
||||
self._urlFetcher = H5WebSocketDebugUrlFetcher(device=self._device)
|
||||
url = self._urlFetcher.fetchWebSocketDebugUrl()
|
||||
|
||||
self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
|
||||
self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer, self._executor, self)
|
||||
self._networkHandler.connect()
|
||||
|
||||
self._executor.put(self._networkHandler.receive)
|
||||
self.logger = Log().getLogger()
|
||||
self.initPageDisplayData()
|
||||
self._hasInit = True
|
||||
self.logger.info('url ----> ' + url)
|
||||
|
||||
def _selectDevice(self):
|
||||
devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
|
||||
# 假如没有指定设备,那么判断当前机器是否连接1个设备
|
||||
if self._device is None:
|
||||
devicesCount = len(devicesList)
|
||||
errorMsg = None
|
||||
|
||||
if devicesCount <= 0:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_DEVICE_NOT_CONNECT)
|
||||
|
||||
elif devicesCount == 1:
|
||||
self._device = devicesList[0]
|
||||
|
||||
else:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_MULTIPLE_DEVICE)
|
||||
|
||||
if errorMsg is not None:
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
def wait(self, seconds=1):
|
||||
time.sleep(seconds)
|
||||
|
||||
def _onUncaughtException(self, exctype, value, tb):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._networkHandler is not None:
|
||||
self._networkHandler.quit()
|
||||
|
||||
if self._executor is not None:
|
||||
self._executor.shutDown()
|
||||
|
||||
self._hasInit = False
|
||||
|
||||
UncaughtExceptionHandler.removeHook()
|
||||
|
||||
def addShutDownHook(self, func, *args, **kwargs):
|
||||
self._vmShutdownHandler.registerVMShutDownCallback(func, *args, **kwargs)
|
||||
|
||||
def getDriverType(self):
|
||||
return constant.H5DRIVER
|
||||
|
||||
def switchToNextPage(self):
|
||||
"""
|
||||
把之前缓存的html置为none
|
||||
再重新连接websocket
|
||||
"""
|
||||
self.logger.debug('')
|
||||
self.html = None
|
||||
self._networkHandler.disconnect()
|
||||
self._networkHandler.connect()
|
||||
|
||||
def initPageDisplayData(self):
|
||||
driverInfo = self.d.info
|
||||
displayDp = driverInfo.get('displaySizeDpY')
|
||||
displayPx = driverInfo.get('displayHeight')
|
||||
self.scale = (displayPx - 0.5) / displayDp
|
||||
windowHeight = self.getWindowHeight()
|
||||
self.appTitleHeight = displayDp - windowHeight
|
||||
|
||||
def changeDp2Px(self, xDp, yDp):
|
||||
xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale, self.appTitleHeight)
|
||||
return xPx, yPx
|
||||
|
||||
def clickElementByXpath(self, xpath, visibleItemXpath=None, duration=50, tapCount=1):
|
||||
"""
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
# 防止websocket未失效但页面已经开始跳转
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
if self.isElementExist(xpath):
|
||||
self.scrollToElementByXpath(xpath, visibleItemXpath)
|
||||
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
x = self._getRelativeDirectionValue("x")
|
||||
y = self._getRelativeDirectionValue("y")
|
||||
|
||||
self.logger.debug('clickElementByXpath --> x:' + str(x) + ' y:' + str(y))
|
||||
clickCommand = self._pageOperator.clickElementByXpath(x, y, duration, tapCount)
|
||||
return self._networkHandler.send(clickCommand)
|
||||
|
||||
def clickFirstElementByText(self, text, visibleItemXpath=None, duration=50, tapCount=1):
|
||||
"""
|
||||
通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
|
||||
"""
|
||||
self.clickElementByXpath('.//*[text()="' + text + '"]', visibleItemXpath, duration, tapCount)
|
||||
|
||||
def longClickElementByXpath(self, xpath, visibleItemXpath=None, duration=2000, tapCount=1):
|
||||
self.clickElementByXpath(xpath, visibleItemXpath, duration, tapCount)
|
||||
|
||||
def repeatedClickElementByXpath(self, xpath, visibleItemXpath=None, duration=50, tapCount=2):
|
||||
self.clickElementByXpath(xpath, visibleItemXpath, duration, tapCount)
|
||||
|
||||
def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=400):
|
||||
"""
|
||||
滑动屏幕,使指定xpath的控件可见
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
top = self._getRelativeDirectionValue("topp")
|
||||
bottom = self._getRelativeDirectionValue("bottom")
|
||||
left = self._getRelativeDirectionValue("left")
|
||||
right = self._getRelativeDirectionValue("right")
|
||||
|
||||
if visibleItemXpath is None:
|
||||
endTop = 0
|
||||
endLeft = 0
|
||||
endBottom = self.getWindowHeight()
|
||||
endRight = self.getWindowWidth()
|
||||
else:
|
||||
containerSendStr = self._pageOperator.getElementRect(visibleItemXpath)
|
||||
self._networkHandler.send(containerSendStr)
|
||||
|
||||
endTop = self._getRelativeDirectionValue("topp")
|
||||
endBottom = self._getRelativeDirectionValue("bottom")
|
||||
endLeft = self._getRelativeDirectionValue("left")
|
||||
endRight = self._getRelativeDirectionValue("right")
|
||||
|
||||
'''
|
||||
竖直方向的滑动
|
||||
'''
|
||||
if bottom > endBottom:
|
||||
scrollYDistance = endBottom - bottom
|
||||
elif top < 0:
|
||||
scrollYDistance = -(top - endTop)
|
||||
else:
|
||||
scrollYDistance = 0
|
||||
|
||||
if scrollYDistance < 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance - 80,
|
||||
speed)
|
||||
elif scrollYDistance > 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance + 80,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('y方向不需要滑动')
|
||||
|
||||
'''
|
||||
水平方向的滑动
|
||||
'''
|
||||
if right > endRight:
|
||||
scrollXDistance = endRight - right
|
||||
elif left < 0:
|
||||
scrollXDistance = -(left - endLeft)
|
||||
else:
|
||||
scrollXDistance = 0
|
||||
|
||||
if scrollXDistance != 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), scrollXDistance, 0,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('y方向不需要滑动')
|
||||
|
||||
def getElementCoordinateByXpath(self, elementXpath):
|
||||
'''
|
||||
获得Element的坐标
|
||||
:param elementXpath:待获取坐标的元素的xpath
|
||||
:return:element相对于整个屏幕的x、y坐标,单位为px
|
||||
'''
|
||||
self.logger.info(
|
||||
'elementXpathXpath ---> ' + elementXpath)
|
||||
# 防止websocket未失效但页面已经开始跳转
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
if self.isElementExist(elementXpath):
|
||||
sendStr = self._pageOperator.getElementRect(elementXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
x = self._getRelativeDirectionValue("x")
|
||||
y = self._getRelativeDirectionValue("y")
|
||||
x, y = self.changeDp2Px(x, y)
|
||||
return x, y
|
||||
errorMessage = ErrorMsgManager().errorCodeToString(ERROR_CODE_GETCOORDINATE)
|
||||
raise RuntimeError(errorMessage)
|
||||
|
||||
def clearInputTextByXpath(self, xpath):
|
||||
'''
|
||||
清空输入框的文字
|
||||
:param xpath:input框的xpath
|
||||
'''
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
clearInputTextSendStr = self._pageOperator.clearInputTextByXpath(xpath)
|
||||
self._networkHandler.send(clearInputTextSendStr)
|
||||
|
||||
def getWindowHeight(self):
|
||||
'''
|
||||
:return:手机屏幕的高度
|
||||
'''
|
||||
getWindowHeightCmd = self._pageOperator.getWindowHeight()
|
||||
resultValueDict = self._networkHandler.send(getWindowHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getWindowWidth(self):
|
||||
'''
|
||||
:return:手机屏幕的宽度
|
||||
'''
|
||||
getWindowWidthCmd = self._pageOperator.getWindowWidth()
|
||||
resultValueDict = self._networkHandler.send(getWindowWidthCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
|
||||
"""
|
||||
通过坐标来滑动(屏幕的左上角为(0,0),向下和向右坐标逐渐增大)
|
||||
:param x: 滑动的起始X点坐标
|
||||
:param y: 滑动的起始Y点坐标
|
||||
:param xDistance: X方向滑动的距离
|
||||
:param yDistance: Y方向滑动的距离
|
||||
:param speed: 滑动的速度
|
||||
"""
|
||||
sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance, speed)
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def _getRelativeDirectionValue(self, directionKey='topp', contextId=None):
|
||||
'''
|
||||
获取相关的方向数据参数值
|
||||
:param directionKey: 获取的方向
|
||||
:return:
|
||||
'''
|
||||
directionCommand = self._pageOperator.getJSValue(directionKey, contextId)
|
||||
directionResponse = self._networkHandler.send(directionCommand).getResponse()[0]
|
||||
directionValue = directionResponse['result']['result']['value']
|
||||
return directionValue
|
||||
|
||||
def textElementByXpath(self, xpath, text):
|
||||
"""
|
||||
先获取输入框的焦点, 再使用Chrome debug协议的输入api,再输入文字内容
|
||||
:param xpath:输入框的xpath
|
||||
:parm text:要输入的文字
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
|
||||
self.focusElementByXpath(xpath)
|
||||
sendStrList = self._pageOperator.textElementByXpath(text)
|
||||
|
||||
for command in sendStrList:
|
||||
self._networkHandler.send(command)
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def focusElementByXpath(self, xpath):
|
||||
"""
|
||||
调用目标的focus()方法。
|
||||
:param xpath:目标的xpath
|
||||
"""
|
||||
executeCmd = self._pageOperator.focusElementByXpath(xpath)
|
||||
self._networkHandler.send(executeCmd)
|
||||
|
||||
def getHtml(self, nodeId=1):
|
||||
"""
|
||||
获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
|
||||
:param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
|
||||
"""
|
||||
self.logger.info('')
|
||||
if self.html is not None:
|
||||
self.switchToNextPage()
|
||||
|
||||
self.getDocument()
|
||||
|
||||
sendStr = self._pageOperator.getHtml(nodeId)
|
||||
self.html = self._networkHandler.send(sendStr).getResponse()[0]['result']['outerHTML']
|
||||
return self.html
|
||||
|
||||
def getDocument(self):
|
||||
"""
|
||||
获得getHtml中需要的nodeId
|
||||
在调用getHtml之前必须先调用这个方法
|
||||
"""
|
||||
sendStr = self._pageOperator.getDocument()
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def closeWindow(self):
|
||||
"""
|
||||
关闭整个h5页面
|
||||
"""
|
||||
sendStr = self._pageOperator.closeWindow()
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def returnLastPage(self):
|
||||
"""
|
||||
返回上一页
|
||||
"""
|
||||
self.logger.info('')
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
sendStr = self._pageOperator.returnLastPage()
|
||||
self._networkHandler.send(sendStr)
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def scrollPickerByXpath(self, xpath):
|
||||
"""
|
||||
滑动选择picker的选项
|
||||
要获取四个变量,所以发送了四次消息。
|
||||
1.先定位到要点击的item
|
||||
2.找到它的parent(即整个picker的content区域),并且获得要点击item的index,和总共item的个数
|
||||
3.获得picker的BoundingClientRect。再计算每个item的位置,要滑动的距离
|
||||
4.因为picker窗口弹出时,默认选择上一次的item,所以先进行一次滑动到顶端。
|
||||
(为了避免滑动时的惯性,需要设置一个speed属性,控制速度)
|
||||
:param xpath: 要选择的item
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
|
||||
sendStr = self._pageOperator.getPickerRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
startScrollX = self._getRelativeDirectionValue("start_scroll_x")
|
||||
startScrollY = self._getRelativeDirectionValue("start_scroll_y")
|
||||
|
||||
# 获取整个列表的高度
|
||||
height = self._getRelativeDirectionValue("height")
|
||||
|
||||
# 获取要滑动的距离
|
||||
dex = self._getRelativeDirectionValue("dex")
|
||||
|
||||
scrollWindowCmd = self._pageOperator.scrollWindow(startScrollX, startScrollY, startScrollX, height)
|
||||
self._networkHandler.send(scrollWindowCmd)
|
||||
|
||||
scrollWIndowWithSpeedCmd = self._pageOperator.scrollWindow(startScrollX, startScrollY, startScrollX, -dex,
|
||||
speed=72)
|
||||
self._networkHandler.send(scrollWIndowWithSpeedCmd)
|
||||
|
||||
def getPageHeight(self):
|
||||
getPageHeightCmd = self._pageOperator.getPageHeight()
|
||||
resultValueDict = self._networkHandler.send(getPageHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getElementTextByXpath(self, xpath):
|
||||
'''
|
||||
:param xpath: 目标的xpath
|
||||
:return: 获取到的目标text内容
|
||||
'''
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
if self.isElementExist(xpath):
|
||||
getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getTextCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementSrcByXpath(self, xpath):
|
||||
"""
|
||||
:param xpath: 目标的xpath
|
||||
:return: 获取到的目标src内容
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
if self.isElementExist(xpath):
|
||||
getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getSrcCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementClassNameByXpath(self, xpath):
|
||||
'''
|
||||
:param xpath:目标的xpath
|
||||
:return: 目标的className
|
||||
'''
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
if self.isElementExist(xpath):
|
||||
getClassNameCmd = self._pageOperator.getElementClassNameByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getClassNameCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementByXpath(self, xpath):
|
||||
"""
|
||||
:param:目标的xpath
|
||||
:return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
|
||||
例如:可以使用element.get("attrs")来拿到属性的数据
|
||||
可以用element.text拿到它的文字
|
||||
当element中含有列表时,使用for循环读取每一个item
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
html = etree.HTML(self.getHtml())
|
||||
return html.xpath(xpath)[0]
|
||||
|
||||
def isElementExist(self, xpath, contextId=None):
|
||||
"""
|
||||
:param xpath: 目标的xpath
|
||||
:return: 返回一个boolean,该Element是否存在
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath, contextId)
|
||||
resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
num = 0
|
||||
while resultType == 'null' and num < 3:
|
||||
self.wait(WAIT_REFLESH_2_SECOND)
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath, contextId)
|
||||
resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
num = num + 1
|
||||
return resultType != 'null'
|
||||
|
||||
def navigateToPage(self, url):
|
||||
"""
|
||||
跳转到指定url,在某些微信版本上不生效
|
||||
:param url: 要跳转的url
|
||||
"""
|
||||
self.logger.info('url ---> ' + url)
|
||||
navigateCmd = self._pageOperator.navigateToPage(url)
|
||||
self._networkHandler.send(navigateCmd)
|
||||
|
||||
def executeScript(self, script):
|
||||
"""
|
||||
手动发送js命令并执行
|
||||
:param script:要执行的js指令
|
||||
:return: 执行结果
|
||||
"""
|
||||
executeCmd = self._pageOperator.executeScript(script)
|
||||
resultValueDict = self._networkHandler.send(executeCmd).getResponse()[0]
|
||||
|
||||
return resultValueDict
|
||||
|
||||
def getCurrentPageUrl(self):
|
||||
"""
|
||||
获得当前页面的url
|
||||
:return:
|
||||
"""
|
||||
executeCmd = self._pageOperator.getCurrentPageUrl()
|
||||
resultValueDict = self._networkHandler.send(executeCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
return resultValue
|
||||
|
||||
'''
|
||||
针对跨域IFrame进行的操作
|
||||
'''
|
||||
|
||||
def _getAllNodeId(self):
|
||||
'''
|
||||
获得body标签中所有的包含IFrame数据的NodeId
|
||||
:return:
|
||||
'''
|
||||
nodeIdList = []
|
||||
nodesList = self.getBodyNode()['params']['nodes']
|
||||
for node in nodesList:
|
||||
if node.get('contentDocument') is not None:
|
||||
nodeIdList.append(node.get('contentDocument').get('nodeId'))
|
||||
return nodeIdList
|
||||
|
||||
def getBodyNode(self):
|
||||
'''
|
||||
:return:获得body中所有node的frameId
|
||||
'''
|
||||
self.switchToNextPage()
|
||||
self.wait(WAIT_REFLESH_2_SECOND)
|
||||
self.getDocument()
|
||||
executeCmd = self._pageOperator.getBodyNode()
|
||||
resultValueDict = self._networkHandler.send(executeCmd).getResponse()[0]
|
||||
return resultValueDict
|
||||
|
||||
def requestChildNodes(self, nodeId=5):
|
||||
'''
|
||||
:param nodeId:指定的nodeId
|
||||
:return:
|
||||
'''
|
||||
self.switchToNextPage()
|
||||
self.wait(WAIT_REFLESH_2_SECOND)
|
||||
self.getDocument()
|
||||
executeCmd = self._pageOperator.requestChildNodes(nodeId)
|
||||
resultValueDict = self._networkHandler.send(executeCmd).getResponse()[0]
|
||||
return resultValueDict
|
||||
|
||||
def getIFrameContextId(self):
|
||||
'''
|
||||
当body标签中只存在一个IFrame调用
|
||||
:return:ContextId
|
||||
'''
|
||||
time.sleep(5)
|
||||
frameIdList = self.getAllIFrameNode()
|
||||
contextList = self.getAllContext()
|
||||
if len(frameIdList) == 1:
|
||||
for contextInfo in contextList:
|
||||
if contextInfo["frameId"] == frameIdList[0]:
|
||||
return contextInfo["contextId"]
|
||||
|
||||
def getAllContext(self):
|
||||
'''
|
||||
获得所有的Context
|
||||
:return: 所有的ContextId,域以及FrameId
|
||||
'''
|
||||
executeCmd = self._pageOperator.getAllContext()
|
||||
resultValueDict = self._networkHandler.send(executeCmd).getResponse()
|
||||
resultDictList = []
|
||||
for dict in resultValueDict:
|
||||
if dict.get('result') is None:
|
||||
resultDict = {}
|
||||
if dict.get('params').get('context') is not None:
|
||||
context = dict.get('params').get('context')
|
||||
origin = context.get('origin')
|
||||
contextId = context.get('id')
|
||||
frameId = context.get('auxData').get('frameId')
|
||||
resultDict['origin'] = origin
|
||||
resultDict['contextId'] = contextId
|
||||
resultDict['frameId'] = frameId
|
||||
resultDictList.append(resultDict)
|
||||
return resultDictList
|
||||
|
||||
def getAllIFrameNode(self):
|
||||
'''
|
||||
获得body标签中所有的IFrameNode
|
||||
:return:
|
||||
'''
|
||||
try:
|
||||
iFrameNodeList = []
|
||||
nodesList = self.getBodyNode()['params']['nodes']
|
||||
for node in nodesList:
|
||||
if node['nodeName'] == 'IFRAME':
|
||||
iFrameNodeList.append(node)
|
||||
frameIdList = []
|
||||
for iFrameNode in iFrameNodeList:
|
||||
frameIdList.append(iFrameNode['frameId'])
|
||||
return frameIdList
|
||||
except:
|
||||
errorMessage = ErrorMsgManager().errorCodeToString(ERROR_CODE_SETUP_FRAME_PAGE)
|
||||
raise RuntimeError(errorMessage)
|
||||
|
||||
def getIFrameNodeId(self):
|
||||
'''
|
||||
当body标签中只存在一个IFrame调用
|
||||
:return:IFrame的NodeId,可以通过它获得html
|
||||
'''
|
||||
nodeIdList = self._getAllNodeId()
|
||||
if len(nodeIdList) == 1:
|
||||
return nodeIdList[0]
|
||||
|
||||
def getIFrameElementCoordinateByXpath(self, elementXpath, iFrameXpath, contextId):
|
||||
'''
|
||||
获得IFrame中元素的坐标
|
||||
:param elementXpath:待获取坐标的元素的xpath
|
||||
:param iFrameXpath: 外层iFrame的xpath
|
||||
:param contextId: iFrame的ContextId
|
||||
:return:element相对于整个屏幕的x、y坐标,单位为px
|
||||
'''
|
||||
self.logger.info(
|
||||
'elementXpathXpath ---> ' + elementXpath + ' iFrameXpath ---> ' + iFrameXpath + ' contextId ---> ' + str(
|
||||
contextId))
|
||||
# 防止websocket未失效但页面已经开始跳转
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
if self.isElementExist(iFrameXpath):
|
||||
sendStr = self._pageOperator.getElementRect(iFrameXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
iframeLeft = self._getRelativeDirectionValue("left")
|
||||
iframeTop = self._getRelativeDirectionValue("topp")
|
||||
if self.isElementExist(elementXpath, contextId):
|
||||
sendStr = self._pageOperator.getElementRect(elementXpath, contextId)
|
||||
self._networkHandler.send(sendStr)
|
||||
x = self._getRelativeDirectionValue("x", contextId)
|
||||
y = self._getRelativeDirectionValue("y", contextId)
|
||||
x = iframeLeft + x
|
||||
y = iframeTop + y
|
||||
x, y = self.changeDp2Px(x, y)
|
||||
return x, y
|
||||
errorMessage = ErrorMsgManager().errorCodeToString(ERROR_CODE_GETCOORDINATE)
|
||||
raise RuntimeError(errorMessage)
|
||||
|
||||
def getIFrameHtml(self, nodeId=None):
|
||||
'''
|
||||
:param nodeId:
|
||||
:return: 获得指定nodeId的Html
|
||||
'''
|
||||
|
||||
self.logger.info('')
|
||||
|
||||
self.switchToNextPage()
|
||||
|
||||
self.getDocument()
|
||||
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
sendStr = self._pageOperator.requestChildNodes()
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
if nodeId is None:
|
||||
nodeId = self.getIFrameNodeId()
|
||||
sendStr = self._pageOperator.getHtml(nodeId)
|
||||
|
||||
result = self._networkHandler.send(sendStr).getResponse()[0]
|
||||
self.html = result.get('result').get('outerHTML')
|
||||
return self.html
|
||||
|
||||
def getIFrameElementByXpath(self, xpath, nodeId):
|
||||
'''
|
||||
:param xpath:element的Xpath
|
||||
:param nodeId: element所在页面的nodeId
|
||||
:return: lxml的ELement对象
|
||||
'''
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
html = etree.HTML(self.getIFrameHtml(nodeId))
|
||||
self.logger.info(etree.tostring(html))
|
||||
return html.xpath(xpath)[0]
|
||||
|
||||
'''
|
||||
性能数据
|
||||
'''
|
||||
|
||||
def getMemoryInfo(self):
|
||||
'''
|
||||
获得H5进程占用内存信息
|
||||
:return: H5进程占用内存信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:tools'
|
||||
return commandHelper.runCommand(GET_MEMORY_INFO)
|
||||
|
||||
def getCPUInfo(self):
|
||||
'''
|
||||
获得H5进程占用CPU信息
|
||||
:return: H5进程占用CPU信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:tools'
|
||||
return commandHelper.runCommand(GET_CPU_INFO)
|
||||
|
||||
def setDebugLogMode(self):
|
||||
Log().setDebugLogMode()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,179 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from fastAutoTest.core.common.command.commandProcessor import CommandProcessor
|
||||
from fastAutoTest.core.h5 import h5UserAPI
|
||||
import json
|
||||
|
||||
|
||||
class H5PageOperator(object):
|
||||
processor = CommandProcessor('h5')
|
||||
|
||||
def addContextIdInParams(self, command, contextId):
|
||||
executeResult = json.loads(command)
|
||||
executeResult['params']['contextId'] = contextId
|
||||
return json.dumps(executeResult)
|
||||
|
||||
def changeDp2Px(self, xDp, yDp, scale, appTitleHeight):
|
||||
xPx = int(xDp * scale + 0.5)
|
||||
yPx = int((yDp + appTitleHeight) * scale + 0.5)
|
||||
return xPx, yPx
|
||||
|
||||
def clickElementByName(self, name, index):
|
||||
params = {"name": name, "index": index}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.NAME, h5UserAPI.ActionType.CLICK, **params)
|
||||
|
||||
def clickElementByXpath(self, x, y, duration=50, tapCount=1):
|
||||
params = {"x": x, "y": y, "duration": duration, "tapCount": tapCount}
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.CLICK, **params)
|
||||
|
||||
def clickElementById(self, id):
|
||||
params = {"id": id}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.ID, h5UserAPI.ActionType.CLICK, **params)
|
||||
|
||||
def textElementByXpath(self, text):
|
||||
"""
|
||||
模拟硬件输入分为三个步骤
|
||||
1.发送消息模拟点击
|
||||
2.发送消息模拟输入(最多只能输入4个字符,因此要对输入的text进行分割 )
|
||||
3.发送消息模拟抬起
|
||||
"""
|
||||
commandList = []
|
||||
|
||||
# downEvent的Command
|
||||
rawKeyDownParams = {"type": "rawKeyDown", "text": ''}
|
||||
keydownCommand = self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.TEXT, **rawKeyDownParams)
|
||||
|
||||
commandList.append(keydownCommand)
|
||||
|
||||
# textEvent的command,需要将字符串分割成每四个字符为一小段
|
||||
unicodeText = unicode(text, 'utf-8')
|
||||
length = len(unicodeText)
|
||||
i = length / 4 if length % 4 == 0 else length / 4 + 1
|
||||
for j in range(i):
|
||||
inputtext = unicodeText[4 * j: 4 * (j + 1)]
|
||||
charParams = {"type": "char", "text": inputtext}
|
||||
charCommand = self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.TEXT, **charParams)
|
||||
commandList.append(charCommand)
|
||||
|
||||
# upEvent的command
|
||||
keyUpParams = {"type": "keyUp", "text": ''}
|
||||
keyupCommand = self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.TEXT, **keyUpParams)
|
||||
commandList.append(keyupCommand)
|
||||
|
||||
return commandList
|
||||
|
||||
def getPickerRect(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.GET_PICKER_RECT,
|
||||
**params)
|
||||
|
||||
def getPageHeight(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.GET_PAGE_HEIGHT)
|
||||
|
||||
def isElementExist(self, xpath, contextId=None):
|
||||
params = {"xpath": xpath}
|
||||
result = self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.IS_ELEMENT_EXIST,
|
||||
**params)
|
||||
if contextId is not None:
|
||||
result = self.addContextIdInParams(result, contextId)
|
||||
return result
|
||||
|
||||
def focusElementByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.FOCUS, **params)
|
||||
|
||||
def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
|
||||
params = {"x": x, "y": y, "xDistance": xDistance, "yDistance": yDistance, "speed": speed}
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.SCROLL, **params)
|
||||
|
||||
def getHtml(self, nodeId):
|
||||
params = {"nodeId": nodeId}
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.GET_HTML, **params)
|
||||
|
||||
def getJSValue(self, value, contextId=None):
|
||||
params = {"value": value}
|
||||
result = self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.GET_JS_VALUE, **params)
|
||||
|
||||
if contextId is not None:
|
||||
result = self.addContextIdInParams(result, contextId)
|
||||
|
||||
return result
|
||||
|
||||
def closeWindow(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.CLOSE_WINDOW)
|
||||
|
||||
def getDocument(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.GET_DOCUMENT)
|
||||
|
||||
def returnLastPage(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.RETURN_LAST_PAGE)
|
||||
|
||||
def navigateToPage(self, url):
|
||||
params = {"url": url}
|
||||
return self.processor.doCommandWithoutElement(
|
||||
h5UserAPI.ActionType.NAVIGATE_PAGE, **params)
|
||||
|
||||
def executeScript(self, script, contextId=None):
|
||||
script = {"script": script}
|
||||
result = self.processor.doCommandWithoutElement(h5UserAPI.ActionType.EXECUTE_SCRIPT, **script)
|
||||
if contextId is not None:
|
||||
result = self.addContextIdInParams(result, contextId)
|
||||
return result
|
||||
|
||||
def getElementTextByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.GET_ELEMENT_TEXT,
|
||||
**params)
|
||||
|
||||
def getElementSrcByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.GET_ELEMENT_SRC,
|
||||
**params)
|
||||
|
||||
def getElementClassNameByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.GET_ELEMENT_CLASS_NAME,
|
||||
**params)
|
||||
|
||||
def getElementRect(self, xpath, contextId=None):
|
||||
params = {"xpath": xpath}
|
||||
result = self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.GET_ELEMENT_RECT,
|
||||
**params)
|
||||
if contextId is not None:
|
||||
result = self.addContextIdInParams(result, contextId)
|
||||
return result
|
||||
|
||||
def getWindowHeight(self):
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.GET_WINDOW_HEIGHT)
|
||||
|
||||
def getWindowWidth(self):
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.GET_WINDOW_WIDTH)
|
||||
|
||||
def getCurrentPageUrl(self):
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.GET_PAGE_URL)
|
||||
|
||||
def clearInputTextByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(h5UserAPI.ByType.XPATH, h5UserAPI.ActionType.CLEAR_INPUT_TEXT,
|
||||
**params)
|
||||
|
||||
def getAllContext(self):
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.GET_ALL_CONTEXT)
|
||||
|
||||
def getBodyNode(self):
|
||||
params = {'nodeId': 5}
|
||||
return self.processor.doCommandWithoutElement(h5UserAPI.ActionType.GET_BODY_NODE, **params)
|
||||
|
||||
def requestChildNodes(self, nodeId=5):
|
||||
params = {"nodeId": nodeId}
|
||||
result = self.processor.doCommandWithoutElement(h5UserAPI.ActionType.REQUEST_CHILD_NODES,
|
||||
**params)
|
||||
return result
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class ByType(object):
|
||||
ID = "id"
|
||||
NAME = "name"
|
||||
XPATH = "xpath"
|
||||
|
||||
|
||||
class ActionType(object):
|
||||
CLICK = "click"
|
||||
TEXT = "text"
|
||||
SCROLL = "scroll"
|
||||
GET_DOCUMENT = "getDocument"
|
||||
GET_HTML = "getHTML"
|
||||
RETURN_LAST_PAGE = "returnPage"
|
||||
CLOSE_WINDOW = "closeWindow"
|
||||
GET_PICKER_RECT = "getRect"
|
||||
GET_JS_VALUE = "getJsValue"
|
||||
GET_ELEMENT_TEXT = "getElementText"
|
||||
GET_ELEMENT_SRC = "getElementSrc"
|
||||
GET_ELEMENT_CLASS_NAME = "getElementClassName"
|
||||
IS_ELEMENT_EXIST = 'isElementExist'
|
||||
NAVIGATE_PAGE = 'navigatePage'
|
||||
GET_PAGE_HEIGHT = 'getPageHeight'
|
||||
EXECUTE_SCRIPT = 'executeScript'
|
||||
FOCUS = 'focus'
|
||||
GET_ELEMENT_RECT = 'getElementRect'
|
||||
GET_WINDOW_HEIGHT = 'getWindowHeight'
|
||||
GET_WINDOW_WIDTH = 'getWindowWidth'
|
||||
CLEAR_INPUT_TEXT = 'clearInputText'
|
||||
GET_PAGE_URL = 'getPageUrl'
|
||||
GET_ALL_CONTEXT = 'getAllContext'
|
||||
GET_BODY_NODE = 'getBodyNode'
|
||||
REQUEST_CHILD_NODES = 'requestChildNodes'
|
|
@ -0,0 +1,160 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import *
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.commandHelper import runCommand
|
||||
from fastAutoTest.utils.common import OS
|
||||
from fastAutoTest.utils.logger import Log
|
||||
import time
|
||||
|
||||
_ADB_FIND_STR_CMD = {
|
||||
"Darwin": "adb shell ps | grep com.tencent.mm:tools", # Mac os 下查找字符串是grep
|
||||
"Linux": "adb shell ps | grep com.tencent.mm:tools", # Mac os 下查找字符串是grep
|
||||
"Windows": "adb shell ps | findstr com.tencent.mm:tools" # windows 下查找字符串是findstr
|
||||
}
|
||||
|
||||
|
||||
class H5WebSocketDebugUrlFetcher(object):
|
||||
DEFAULT_LOCAL_FORWARD_PORT = 9222
|
||||
|
||||
def __init__(self, device, localForwardPort=None):
|
||||
self._device = device
|
||||
self._localForwardPort = localForwardPort if localForwardPort is not None \
|
||||
else H5WebSocketDebugUrlFetcher.DEFAULT_LOCAL_FORWARD_PORT
|
||||
self._webSocketDebugUrl = None
|
||||
|
||||
def fetchWebSocketDebugUrl(self, refetch=False):
|
||||
# 如果没有获取或者需要重新获取debug url,那么获取一次,否则直接返回
|
||||
if self._webSocketDebugUrl is None or refetch:
|
||||
self._fetchInner()
|
||||
|
||||
return self._webSocketDebugUrl
|
||||
|
||||
def getDevice(self):
|
||||
return self._device
|
||||
|
||||
def getForwardPort(self):
|
||||
return self._localForwardPort
|
||||
|
||||
def _fetchInner(self):
|
||||
# 先获取微信Tools进程Pid
|
||||
pid = H5WebSocketDebugUrlFetcher._fetchWeixinToolsProcessPid(device=self._device)
|
||||
|
||||
# 重定向端口
|
||||
H5WebSocketDebugUrlFetcher._forwardLocalPort(self._localForwardPort, pid, device=self._device)
|
||||
|
||||
# 获取本地http://localhost:{重定向端口}/json返回的json数据,提取里面的webSocketDebuggerUrl字段值
|
||||
self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(self._localForwardPort)
|
||||
|
||||
@staticmethod
|
||||
def _fetchWeixinToolsProcessPid(device=None):
|
||||
|
||||
"""
|
||||
|
||||
PS命名结果字段格式为8元祖,格式为:USER PID PPID VSIZE RSS WCHAN PC NAME
|
||||
|
||||
各字段解释
|
||||
USER: 进程的当前用户;
|
||||
PID : 毫无疑问, process ID的缩写,也就进程号;
|
||||
PPID :process parent ID,父进程ID
|
||||
VSIZE : virtual size,进程虚拟地址空间大小;
|
||||
RSS : 进程正在使用的物理内存的大小;
|
||||
WCHAN :进程如果处于休眠状态的话,在内核中的地址;
|
||||
PC : program counter,
|
||||
NAME: process name,进程的名称
|
||||
|
||||
"""
|
||||
osName = OS.getPlatform()
|
||||
cmd = _ADB_FIND_STR_CMD[osName]
|
||||
|
||||
stdout = H5WebSocketDebugUrlFetcher._getProcessInfo(cmd, device)
|
||||
|
||||
weixinProcessInfoLine = None
|
||||
for processInfo in stdout.split("\r\r\n"):
|
||||
if "com.tencent.mm:tools" in processInfo:
|
||||
weixinProcessInfoLine = processInfo
|
||||
break
|
||||
|
||||
if weixinProcessInfoLine is None:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS)
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
weixinProcessInfo = weixinProcessInfoLine.split()
|
||||
return H5WebSocketDebugUrlFetcher._handlePhoneCompat(weixinProcessInfo)
|
||||
|
||||
@staticmethod
|
||||
def _getProcessInfo(cmd, device):
|
||||
nums = 0
|
||||
retry = True
|
||||
stdout = ''
|
||||
while (retry and nums < 8):
|
||||
try:
|
||||
stdout, stdError = runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
retry = False
|
||||
except:
|
||||
nums = nums + 1
|
||||
time.sleep(1)
|
||||
Log().getLogger().debug('open port mapping ---> retry ' + str(nums))
|
||||
return stdout
|
||||
|
||||
@staticmethod
|
||||
def _handlePhoneCompat(weixinProcessInfo):
|
||||
|
||||
# 这里建立ps命令返回结果行字段数和pid字段index的映射
|
||||
pidIndexMap = {9: 1, # 三星的手机是9元祖,第二列为pid index
|
||||
5: 0, # 华为的手机是5元祖,第一列为pid index
|
||||
10: 5, # 当微信出现异常时可能出现两个tools,那么选择第二个。
|
||||
18: 10
|
||||
}
|
||||
|
||||
fieldCount = len(weixinProcessInfo)
|
||||
pidIndex = pidIndexMap.get(fieldCount, -1)
|
||||
|
||||
if pidIndex >= 0:
|
||||
return int(weixinProcessInfo[pidIndex])
|
||||
else:
|
||||
raise RuntimeError(ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENABLE_DEBUG_MODE))
|
||||
|
||||
@staticmethod
|
||||
def _forwardLocalPort(localPort, pid, device=None):
|
||||
cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (localPort, pid)
|
||||
runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
|
||||
def _fetchWebSocketDebugUrl(self, localPort):
|
||||
import json
|
||||
import urllib2
|
||||
errorMsg = None
|
||||
resultUrl = None
|
||||
|
||||
localUrl = "http://localhost:%s/json" % localPort
|
||||
|
||||
# 去掉代理
|
||||
urllib2.getproxies = lambda: {}
|
||||
|
||||
try:
|
||||
nums = 0
|
||||
while None == resultUrl and nums < 8:
|
||||
response = urllib2.urlopen(localUrl)
|
||||
responseJson = json.loads(response.read())
|
||||
if len(responseJson) != 0:
|
||||
for responseItem in responseJson:
|
||||
descriptionDict = json.loads(responseItem['description'])
|
||||
if descriptionDict['empty'] == False:
|
||||
resultUrl = responseItem["webSocketDebuggerUrl"]
|
||||
return resultUrl
|
||||
else:
|
||||
nums = nums + 1
|
||||
Log().getLogger().debug('retry fetch num ---> ' + str(nums))
|
||||
time.sleep(1)
|
||||
|
||||
except Exception:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_CONFIG_PROXY)
|
||||
if not errorMsg:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENTER_H5)
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,439 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
import uiautomator
|
||||
from lxml import etree
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import ErrorMsgManager, ERROR_CODE_DEVICE_NOT_CONNECT, \
|
||||
ERROR_CODE_MULTIPLE_DEVICE
|
||||
from fastAutoTest.core.common.network.shortLiveWebSocket import ShortLiveWebSocket
|
||||
from fastAutoTest.core.common.network.websocketdatatransfer import WebSocketDataTransfer
|
||||
from fastAutoTest.core.h5.h5PageOperator import H5PageOperator
|
||||
from fastAutoTest.utils import commandHelper
|
||||
from fastAutoTest.utils import constant
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.constant import WAIT_REFLESH_3_SECOND, WAIT_REFLESH_05_SECOND, \
|
||||
WAIT_REFLESH_1_SECOND
|
||||
from fastAutoTest.utils.logger import Log
|
||||
from fastAutoTest.utils.singlethreadexecutor import SingleThreadExecutor
|
||||
from fastAutoTest.utils.vmhook import VMShutDownHandler, UncaughtExceptionHandler
|
||||
from qqWebSocketDebugUrlFetcher import QQWebSocketDebugUrlFetcher
|
||||
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
__all__ = {
|
||||
"QQDriver"
|
||||
}
|
||||
|
||||
|
||||
class QQDriver(object):
|
||||
def initDriver(self):
|
||||
if self._hasInit:
|
||||
return
|
||||
|
||||
self._urlFetcher = QQWebSocketDebugUrlFetcher(device=self._device)
|
||||
url = self._urlFetcher.fetchWebSocketDebugUrl()
|
||||
self._vmShutdownHandler.registerToVM()
|
||||
UncaughtExceptionHandler.init()
|
||||
UncaughtExceptionHandler.registerUncaughtExceptionCallback(self._onUncaughtException)
|
||||
|
||||
self._selectDevice()
|
||||
|
||||
self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
|
||||
self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer, self._executor, self)
|
||||
self._networkHandler.connect()
|
||||
self._executor.put(self._networkHandler.receive)
|
||||
|
||||
self.initPageDisplayData()
|
||||
self.logger = Log().getLogger()
|
||||
self._hasInit = True
|
||||
|
||||
def __init__(self, device=None):
|
||||
self._device = device
|
||||
self._urlFetcher = None
|
||||
self._webSocketDataTransfer = None
|
||||
self._vmShutdownHandler = VMShutDownHandler()
|
||||
self._networkHandler = None
|
||||
self._executor = SingleThreadExecutor()
|
||||
|
||||
self._pageOperator = H5PageOperator()
|
||||
|
||||
self._hasInit = False
|
||||
|
||||
self.d = uiautomator.Device(self._device)
|
||||
self.html = None
|
||||
|
||||
def getDriverType(self):
|
||||
return constant.QQDRIVER
|
||||
|
||||
def initPageDisplayData(self):
|
||||
driverInfo = self.d.info
|
||||
displayDp = driverInfo.get('displaySizeDpY')
|
||||
displayPx = driverInfo.get('displayHeight')
|
||||
self.scale = (displayPx - 0.5) / displayDp
|
||||
windowHeight = self.getWindowHeight()
|
||||
self.appTitleHeight = displayDp - windowHeight
|
||||
|
||||
def changeDp2Px(self, xDp, yDp):
|
||||
xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale, self.appTitleHeight)
|
||||
return xPx, yPx
|
||||
|
||||
def needSwitchNextPage(self):
|
||||
return self._urlFetcher.needSwitchNextPage()
|
||||
|
||||
def _selectDevice(self):
|
||||
devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
|
||||
# 假如没有指定设备,那么判断当前机器是否连接1个设备
|
||||
if self._device is None:
|
||||
devicesCount = len(devicesList)
|
||||
errorMsg = None
|
||||
|
||||
if devicesCount <= 0:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_DEVICE_NOT_CONNECT)
|
||||
|
||||
elif devicesCount == 1:
|
||||
self._device = devicesList[0]
|
||||
|
||||
else:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_MULTIPLE_DEVICE)
|
||||
|
||||
if errorMsg is not None:
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
def switchToNextPage(self):
|
||||
"""
|
||||
需要重新获取当前页面的websocket的url
|
||||
"""
|
||||
self._networkHandler.disconnect()
|
||||
url = self._urlFetcher.fetchWebSocketDebugUrl(refetch=True)
|
||||
self.logger.debug('url -> ' + url)
|
||||
self._webSocketDataTransfer.setUrl(url)
|
||||
self._networkHandler.connect()
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def returnLastPage(self):
|
||||
self.logger.info('')
|
||||
self.wait(WAIT_REFLESH_1_SECOND)
|
||||
self._networkHandler.disconnect()
|
||||
self.switchToNextPage()
|
||||
self._networkHandler.disconnect()
|
||||
self.d.press('back')
|
||||
self.wait(WAIT_REFLESH_1_SECOND)
|
||||
self.switchToNextPage()
|
||||
|
||||
def getDocument(self):
|
||||
"""
|
||||
获得getHtml中需要的nodeId
|
||||
在调用getHtml之前必须先调用这个方法
|
||||
"""
|
||||
sendStr = self._pageOperator.getDocument()
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def getHtml(self, nodeId=1):
|
||||
"""
|
||||
获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
|
||||
:param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
|
||||
"""
|
||||
self.logger.info('')
|
||||
self.switchToNextPage()
|
||||
self.getDocument()
|
||||
sendStr = self._pageOperator.getHtml(nodeId)
|
||||
self.html = self._networkHandler.send(sendStr).getResponse()[0]['result']['outerHTML']
|
||||
return self.html
|
||||
|
||||
def isElementExist(self, xpath):
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath)
|
||||
resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
if resultType == 'null':
|
||||
self.wait(WAIT_REFLESH_3_SECOND)
|
||||
self.switchToNextPage()
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath)
|
||||
resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
self.logger.debug('isElementExist Response --> ' + str(resultValueDict))
|
||||
return resultType != 'null'
|
||||
|
||||
def getElementTextByXpath(self, xpath):
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
self.switchToNextPage()
|
||||
if self.isElementExist(xpath):
|
||||
getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getTextCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementSrcByXpath(self, xpath):
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
self.switchToNextPage()
|
||||
if self.isElementExist(xpath):
|
||||
getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getSrcCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementByXpath(self, xpath):
|
||||
"""
|
||||
:param:目标的xpath
|
||||
:return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
|
||||
例如:可以使用element.get("attrs")来拿到属性的数据
|
||||
可以用element.text拿到它的文字
|
||||
当element中含有列表时,使用for循环读取每一个item
|
||||
"""
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
htmlData = self.getHtml()
|
||||
if htmlData is not None:
|
||||
html = etree.HTML(htmlData)
|
||||
elementList = html.xpath(xpath)
|
||||
if len(elementList) != 0:
|
||||
return elementList[0]
|
||||
else:
|
||||
self.logger.info('找不到xpath为: ' + xpath + ' 的控件')
|
||||
return ''
|
||||
else:
|
||||
self.logger.info('获取到的html为空')
|
||||
return ''
|
||||
|
||||
def textElementByXpath(self, xpath, text):
|
||||
"""
|
||||
先获取输入框的焦点, 再使用Chrome debug协议的输入api,再输入文字内容
|
||||
:param xpath:输入框的xpath
|
||||
:parm text:要输入的文字
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
|
||||
self.focusElementByXpath(xpath)
|
||||
sendStrList = self._pageOperator.textElementByXpath(text)
|
||||
|
||||
for command in sendStrList:
|
||||
self._networkHandler.send(command)
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def clickElementByXpath(self, xpath, visibleItemXpath=None, byUiAutomator=False):
|
||||
"""
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
self.scrollToElementByXpath(xpath, visibleItemXpath)
|
||||
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
x = self._getRelativeDirectionValue('x')
|
||||
y = self._getRelativeDirectionValue('y')
|
||||
self.logger.debug('clickElementByXpath x:' + str(x) + ' y:' + str(y))
|
||||
|
||||
if not byUiAutomator:
|
||||
clickCommand = self._pageOperator.clickElementByXpath(x, y)
|
||||
return self._networkHandler.send(clickCommand)
|
||||
else:
|
||||
xPx, yPx = self.changeDp2Px(x, y)
|
||||
self.d.click(xPx, yPx)
|
||||
|
||||
def clickFirstElementByText(self, text, visibleItemXpath=None, byUiAutomator=False):
|
||||
"""
|
||||
通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
|
||||
"""
|
||||
self.clickElementByXpath('.//*[text()="' + text + '"]', visibleItemXpath, byUiAutomator)
|
||||
|
||||
def focusElementByXpath(self, xpath):
|
||||
"""
|
||||
调用目标的focus()方法。
|
||||
:param xpath:目标的xpath
|
||||
"""
|
||||
executeCmd = self._pageOperator.focusElementByXpath(xpath)
|
||||
self._networkHandler.send(executeCmd)
|
||||
|
||||
def getElementCoordinateByXpath(self, elementXpath):
|
||||
'''
|
||||
获得Element的坐标
|
||||
:param elementXpath:待获取坐标的元素的xpath
|
||||
:return:element相对于整个屏幕的x、y坐标,单位为px
|
||||
'''
|
||||
self.logger.info(
|
||||
'elementXpathXpath ---> ' + elementXpath)
|
||||
|
||||
sendStr = self._pageOperator.getElementRect(elementXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
x = self._getRelativeDirectionValue('x')
|
||||
y = self._getRelativeDirectionValue('y')
|
||||
xPx, yPx = self.changeDp2Px(x, y)
|
||||
return xPx, yPx
|
||||
|
||||
def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=600):
|
||||
"""
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
top = self._getRelativeDirectionValue('topp')
|
||||
bottom = self._getRelativeDirectionValue('bottom')
|
||||
left = self._getRelativeDirectionValue('left')
|
||||
right = self._getRelativeDirectionValue('right')
|
||||
self.logger.debug('scrollToElementByXpath -> top:bottom:left:right = ' + str(top) + " :" + str(bottom) \
|
||||
+ " :" + str(left) + " :" + str(right))
|
||||
|
||||
if visibleItemXpath is None:
|
||||
endTop = 0
|
||||
endLeft = 0
|
||||
endBottom = self.getWindowHeight()
|
||||
endRight = self.getWindowWidth()
|
||||
else:
|
||||
sendStr = self._pageOperator.getElementRect(visibleItemXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
endTop = self._getRelativeDirectionValue('topp')
|
||||
endBottom = self._getRelativeDirectionValue('bottom')
|
||||
endLeft = self._getRelativeDirectionValue('left')
|
||||
endRight = self._getRelativeDirectionValue('right')
|
||||
self.logger.debug(
|
||||
'scrollToElementByXpath -> toendTop:endBottom:endLeft:endRight = ' + str(endTop) + " :" + str(
|
||||
endBottom) \
|
||||
+ " :" + str(endLeft) + " :" + str(endRight))
|
||||
|
||||
'''
|
||||
竖直方向的滑动
|
||||
'''
|
||||
if endBottom < bottom:
|
||||
scrollYDistance = endBottom - bottom
|
||||
elif top < 0:
|
||||
scrollYDistance = -(top - endTop)
|
||||
else:
|
||||
scrollYDistance = 0
|
||||
|
||||
if scrollYDistance != 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('y方向不需要滑动')
|
||||
'''
|
||||
水平方向的滑动
|
||||
'''
|
||||
if right > endRight:
|
||||
scrollXDistance = endRight - right
|
||||
elif left < 0:
|
||||
scrollXDistance = -(left - endLeft)
|
||||
else:
|
||||
scrollXDistance = 0
|
||||
|
||||
if scrollXDistance != 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), scrollXDistance, 0,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('x方向不需要滑动')
|
||||
|
||||
def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
|
||||
self.logger.info('')
|
||||
sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance, speed)
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def _getRelativeDirectionValue(self, directionKey='topp'):
|
||||
'''
|
||||
获取相关的方向数据参数值
|
||||
:param directionKey: key值
|
||||
:return:
|
||||
'''
|
||||
directionCommand = self._pageOperator.getJSValue(directionKey)
|
||||
directionResponse = self._networkHandler.send(directionCommand).getResponse()[0]
|
||||
directionValue = directionResponse['result']['result']['value']
|
||||
return directionValue
|
||||
|
||||
'''
|
||||
性能数据
|
||||
'''
|
||||
|
||||
def getMemoryInfo(self):
|
||||
'''
|
||||
获得小程序进程占用内存信息
|
||||
:return: 小程序进程占用内存信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:appbrand0'
|
||||
return commandHelper.runCommand(GET_MEMORY_INFO)
|
||||
|
||||
def getCPUInfo(self):
|
||||
'''
|
||||
获得小程序进程占用CPU信息
|
||||
:return: 小程序进程占用CPU信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:appbrand0'
|
||||
return commandHelper.runCommand(GET_CPU_INFO)
|
||||
|
||||
def getPageHeight(self):
|
||||
getPageHeightCmd = self._pageOperator.getPageHeight()
|
||||
resultValueDict = self._networkHandler.send(getPageHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getWindowWidth(self):
|
||||
'''
|
||||
:return:手机屏幕的宽度
|
||||
'''
|
||||
getWindowWidthCmd = self._pageOperator.getWindowWidth()
|
||||
resultValueDict = self._networkHandler.send(getWindowWidthCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getWindowHeight(self):
|
||||
'''
|
||||
:return:手机屏幕的高度
|
||||
'''
|
||||
getWindowHeightCmd = self._pageOperator.getWindowHeight()
|
||||
resultValueDict = self._networkHandler.send(getWindowHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def wait(self, seconds):
|
||||
time.sleep(seconds)
|
||||
|
||||
def addShutDownHook(self, func, *args, **kwargs):
|
||||
'''
|
||||
添加当程序正常关闭时的操作,可以进行一些环境清理操作等
|
||||
'''
|
||||
self._vmShutdownHandler.registerVMShutDownCallback(func, *args, **kwargs)
|
||||
|
||||
def _onUncaughtException(self, exctype, value, tb):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._networkHandler is not None:
|
||||
self._networkHandler.quit()
|
||||
|
||||
if self._executor is not None:
|
||||
self._executor.shutDown()
|
||||
|
||||
UncaughtExceptionHandler.removeHook()
|
||||
|
||||
def setDebugLogMode(self):
|
||||
'''
|
||||
开启debug的日志模式
|
||||
'''
|
||||
Log().setDebugLogMode()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,206 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import time
|
||||
|
||||
import bidict
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import *
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.commandHelper import runCommand
|
||||
from fastAutoTest.utils.common import OS
|
||||
from fastAutoTest.utils.logger import Log
|
||||
|
||||
# 找到QQ的Tools的PID
|
||||
_ADB_FIND_QQ_STR_CMD = {
|
||||
"Darwin": "adb shell ps | grep com.tencent.mobileqq:tool", # Mac os 下查找字符串是grep
|
||||
"Linux": "adb shell ps | grep com.tencent.mobileqq:tool", # Mac os 下查找字符串是grep
|
||||
"Windows": "adb shell ps | findstr com.tencent.mobileqq:tool" # windows 下查找字符串是findstr
|
||||
}
|
||||
|
||||
|
||||
_ADB_PACKAGE_QQ = "com.tencent.mobileqq"
|
||||
TYPE_QQ = "qq"
|
||||
|
||||
|
||||
class QQWebSocketDebugUrlFetcher(object):
|
||||
DEFAULT_LOCAL_FORWARD_PORT = 9222
|
||||
|
||||
def __init__(self, device, localForwardPort=None):
|
||||
self._device = device
|
||||
self._localForwardPort = localForwardPort if localForwardPort is not None \
|
||||
else QQWebSocketDebugUrlFetcher.DEFAULT_LOCAL_FORWARD_PORT
|
||||
self._webSocketDebugUrl = None
|
||||
self.type = ''
|
||||
|
||||
self.pageUrlDict = bidict.bidict()
|
||||
self.logger = Log().getLogger()
|
||||
|
||||
def fetchWebSocketDebugUrl(self, refetch=False):
|
||||
# 如果没有获取或者需要重新获取debug url,那么获取一次,否则直接返回
|
||||
if self._webSocketDebugUrl is None or refetch:
|
||||
self._fetchInner()
|
||||
|
||||
return self._webSocketDebugUrl
|
||||
|
||||
def getDevice(self):
|
||||
return self._device
|
||||
|
||||
def getForwardPort(self):
|
||||
return self._localForwardPort
|
||||
|
||||
def _fetchInner(self):
|
||||
|
||||
self.logger.debug('')
|
||||
|
||||
pid = QQWebSocketDebugUrlFetcher._fetchQQToolsProcessPid(device=self._device)
|
||||
QQWebSocketDebugUrlFetcher._forwardLocalPort(self._localForwardPort, pid, device=self._device)
|
||||
|
||||
# 获取本地http://localhost:{重定向端口}/json返回的json数据,提取里面的webSocketDebuggerUrl字段值
|
||||
self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(self._localForwardPort)
|
||||
|
||||
@staticmethod
|
||||
def _fetchQQToolsProcessPid(device=None):
|
||||
|
||||
osName = OS.getPlatform()
|
||||
cmd = _ADB_FIND_QQ_STR_CMD[osName]
|
||||
|
||||
stdout = QQWebSocketDebugUrlFetcher._getProcessInfo(cmd, device)
|
||||
|
||||
QQProcessInfoLine = None
|
||||
for processInfo in stdout.split("\r\r\n"):
|
||||
if "com.tencent.mobileqq:tool" in processInfo:
|
||||
QQProcessInfoLine = processInfo
|
||||
break
|
||||
|
||||
if QQProcessInfoLine is None:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS)
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
QQProcessInfo = QQProcessInfoLine.split()
|
||||
return QQWebSocketDebugUrlFetcher._handlePhoneCompat(QQProcessInfo)
|
||||
|
||||
@staticmethod
|
||||
def _getProcessInfo(cmd, device):
|
||||
nums = 0
|
||||
retry = True
|
||||
stdout = ''
|
||||
while (retry and nums < 8):
|
||||
try:
|
||||
stdout, stdError = runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
retry = False
|
||||
except:
|
||||
nums = nums + 1
|
||||
time.sleep(1)
|
||||
Log().getLogger().debug('open port mapping ---> retry ' + str(nums))
|
||||
return stdout
|
||||
|
||||
@staticmethod
|
||||
def _handlePhoneCompat(processInfo):
|
||||
|
||||
# 这里建立ps命令返回结果行字段数和pid字段index的映射
|
||||
pidIndexMap = {9: 1, # 三星的手机是9元祖,第二列为pid index
|
||||
5: 0, # 华为的手机是5元祖,第一列为pid index
|
||||
10: 5, # 当微信出现异常时可能出现两个tools,那么选择第二个。
|
||||
18: 10
|
||||
}
|
||||
|
||||
fieldCount = len(processInfo)
|
||||
pidIndex = pidIndexMap.get(fieldCount, -1)
|
||||
|
||||
if pidIndex >= 0:
|
||||
return int(processInfo[pidIndex])
|
||||
else:
|
||||
raise RuntimeError(ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENABLE_DEBUG_MODE))
|
||||
|
||||
@staticmethod
|
||||
def _forwardLocalPort(localPort, pid, device=None):
|
||||
cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (localPort, pid)
|
||||
runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
|
||||
def _fetchWebSocketDebugUrl(self, localPort):
|
||||
self.logger.debug('')
|
||||
import json
|
||||
import urllib2
|
||||
errorMsg = None
|
||||
resultUrl = None
|
||||
|
||||
localUrl = "http://localhost:%s/json" % localPort
|
||||
|
||||
# 去掉代理
|
||||
urllib2.getproxies = lambda: {}
|
||||
|
||||
try:
|
||||
nums = 0
|
||||
while None == resultUrl and nums < 8:
|
||||
response = urllib2.urlopen(localUrl)
|
||||
responseJson = json.loads(response.read())
|
||||
if len(responseJson) != 0:
|
||||
resultUrl = self._handleAndReturnWebSocketDebugUrl(responseJson)
|
||||
self.logger.debug('websocket --> ' + resultUrl)
|
||||
return resultUrl
|
||||
else:
|
||||
nums = nums + 1
|
||||
Log().getLogger().debug('retry fetch num ---> ' + str(nums))
|
||||
time.sleep(1)
|
||||
|
||||
except Exception:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_CONFIG_PROXY)
|
||||
if not errorMsg:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENTER_H5)
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
# 按顺序记录打开页面,key为打开页面的顺序,value为打开页面的websocketUrl。
|
||||
def _handleAndReturnWebSocketDebugUrl(self, responseJson):
|
||||
import json
|
||||
responseLenth = 0
|
||||
# 只记录有效页面的数量
|
||||
for i in range(len(responseJson)):
|
||||
response = responseJson[i]
|
||||
if response['type'] == u'page' and json.loads(response['description'])['empty'] == False:
|
||||
responseLenth = responseLenth + 1
|
||||
|
||||
pageUrlDictLength = len(self.pageUrlDict)
|
||||
if pageUrlDictLength == 0:
|
||||
self.logger.debug('pageUrlDictLength == 0:')
|
||||
for i in range(responseLenth):
|
||||
response = responseJson[i]
|
||||
if response['type'] == u'page' and json.loads(response['description'])['empty'] == False:
|
||||
self.pageUrlDict.update({pageUrlDictLength + 1: response['webSocketDebuggerUrl']})
|
||||
return response['webSocketDebuggerUrl']
|
||||
elif responseLenth > pageUrlDictLength:
|
||||
self.logger.debug('responseLenth > pageUrlDictLength')
|
||||
for i in range(responseLenth):
|
||||
response = responseJson[i]
|
||||
if response['type'] == u'page' and json.loads(response['description'])['empty'] == False:
|
||||
if self.pageUrlDict.inv.get(response['webSocketDebuggerUrl']) is None:
|
||||
self.pageUrlDict.update({pageUrlDictLength + 1: response['webSocketDebuggerUrl']})
|
||||
return response['webSocketDebuggerUrl']
|
||||
elif responseLenth < pageUrlDictLength:
|
||||
self.logger.debug('responseLenth < pageUrlDictLength')
|
||||
sencondLastPageUrl = self.pageUrlDict.get(pageUrlDictLength - 1)
|
||||
del self.pageUrlDict[pageUrlDictLength]
|
||||
return sencondLastPageUrl
|
||||
else:
|
||||
self.logger.debug('responseLenth == pageUrlDictLength')
|
||||
lastPageUrl = self.pageUrlDict.get(pageUrlDictLength)
|
||||
return lastPageUrl
|
||||
|
||||
def needSwitchNextPage(self):
|
||||
import json
|
||||
import urllib2
|
||||
localUrl = "http://localhost:9223/json"
|
||||
|
||||
# 去掉代理
|
||||
urllib2.getproxies = lambda: {}
|
||||
|
||||
response = urllib2.urlopen(localUrl)
|
||||
responseJson = json.loads(response.read())
|
||||
return len(responseJson) != len(self.pageUrlDict)
|
||||
|
||||
def getType(self):
|
||||
return self.type
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastAutoTest.core.wx.wxUserAPI import ActionType
|
||||
from fastAutoTest.core.wx.wxUserAPI import ByType
|
||||
|
||||
|
||||
class WxCommandManager(object):
|
||||
# 使用$$可以作为格式化时的转义
|
||||
_elementMap = {
|
||||
ByType.ID: "$$('#$id')[0]",
|
||||
ByType.NAME: "$$('.$name')[$index]",
|
||||
ByType.XPATH: "var xpath ='$xpath';"
|
||||
"xpath_obj = document.evaluate(xpath,document,null, XPathResult.ANY_TYPE, null);"
|
||||
"var button = xpath_obj.iterateNext()"
|
||||
}
|
||||
|
||||
# doCommandWithElement中执行的参数
|
||||
_jsActionMap = {
|
||||
|
||||
ActionType.GET_ELEMENT_RECT: ";left=Math.round(button.getBoundingClientRect().left);"
|
||||
"right=Math.round(button.getBoundingClientRect().right);"
|
||||
"bottom=Math.round(button.getBoundingClientRect().bottom);"
|
||||
"topp=Math.round(button.getBoundingClientRect().top);"
|
||||
"x=Math.round((left+right)/2);"
|
||||
"y=Math.round((topp+bottom)/2);",
|
||||
ActionType.IS_ELEMENT_EXIST: ";button",
|
||||
ActionType.GET_ELEMENT_TEXT: ";button.textContent;",
|
||||
ActionType.GET_ELEMENT_SRC: ";button.getAttribute('src')",
|
||||
}
|
||||
|
||||
_methodMap = {
|
||||
ActionType.GET_DOCUMENT: "DOM.getDocument",
|
||||
ActionType.GET_HTML: "DOM.getOuterHTML",
|
||||
ActionType.SCROLL: "Input.synthesizeScrollGesture",
|
||||
ActionType.CLICK: "Input.synthesizeTapGesture",
|
||||
ActionType.GET_ELEMENT_RECT: "Runtime.evaluate",
|
||||
ActionType.GET_PICKER_RECT: "Runtime.evaluate",
|
||||
ActionType.GET_ELEMENT_TEXT: "Runtime.evaluate",
|
||||
ActionType.GET_ELEMENT_SRC: "Runtime.evaluate",
|
||||
ActionType.GET_PAGE_HEIGHT: "Runtime.evaluate",
|
||||
ActionType.GET_JS_VALUE: "Runtime.evaluate",
|
||||
ActionType.TEXT: "Input.dispatchKeyEvent",
|
||||
ActionType.IS_ELEMENT_EXIST: "Runtime.evaluate",
|
||||
ActionType.GET_WINDOW_HEIGHT: "Runtime.evaluate",
|
||||
ActionType.GET_WINDOW_WIDTH: "Runtime.evaluate"
|
||||
|
||||
}
|
||||
|
||||
# string.Template
|
||||
# jsonConcat最终拼接的模板
|
||||
_paramsMap = {
|
||||
"Runtime.evaluate": '{"expression": "$expression"}',
|
||||
"Input.synthesizeScrollGesture":
|
||||
'{"type": "mouseWheel", "x": $x, "y": $y,"xDistance": $xDistance, "yDistance": $yDistance,"speed":$speed}',
|
||||
"Page.navigate": '{"url":"$url"}',
|
||||
"Input.dispatchKeyEvent": '{"type":"$type","text":"$text","unmodifiedText":"$text"}',
|
||||
"Input.synthesizeTapGesture": '{"x":$x,"y":$y}',
|
||||
"DOM.getDocument": "{''}",
|
||||
"DOM.getOuterHTML": '{"nodeId": $nodeId}',
|
||||
}
|
||||
|
||||
# doCommandWithoutElement 中执行的参数
|
||||
_expressionMap = {
|
||||
ActionType.GET_PAGE_HEIGHT: 'document.body.scrollHeight',
|
||||
ActionType.GET_JS_VALUE: '$value',
|
||||
ActionType.GET_WINDOW_HEIGHT: 'document.documentElement.clientHeight',
|
||||
ActionType.GET_WINDOW_WIDTH: "document.documentElement.clientWidth"
|
||||
|
||||
}
|
||||
|
||||
def getElement(self, actionType, default=None):
|
||||
return self._elementMap.get(actionType, default)
|
||||
|
||||
def getJsAction(self, actionType, default=None):
|
||||
return self._jsActionMap.get(actionType, default)
|
||||
|
||||
def getMethod(self, actionType, default=None):
|
||||
return self._methodMap.get(actionType, default)
|
||||
|
||||
def getParams(self, actionType, default=None):
|
||||
return self._paramsMap.get(actionType, default)
|
||||
|
||||
def getExpression(self, actionType, default=None):
|
||||
return self._expressionMap.get(actionType, default)
|
|
@ -0,0 +1,474 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import uiautomator
|
||||
from lxml import etree
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import ErrorMsgManager, ERROR_CODE_DEVICE_NOT_CONNECT, \
|
||||
ERROR_CODE_MULTIPLE_DEVICE
|
||||
from fastAutoTest.core.common.network.shortLiveWebSocket import ShortLiveWebSocket
|
||||
from fastAutoTest.core.common.network.websocketdatatransfer import WebSocketDataTransfer
|
||||
from fastAutoTest.core.wx.wxPageOperator import WxPageOperator
|
||||
from fastAutoTest.core.wx.wxWebSocketDebugUrlFetcher import WxWebSocketDebugUrlFetcher
|
||||
from fastAutoTest.utils import commandHelper
|
||||
from fastAutoTest.utils import constant
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.common import OS
|
||||
from fastAutoTest.utils.constant import WAIT_REFLESH_3_SECOND, WAIT_REFLESH_05_SECOND, WAIT_REFLESH_1_SECOND
|
||||
from fastAutoTest.utils.logger import Log
|
||||
from fastAutoTest.utils.singlethreadexecutor import SingleThreadExecutor
|
||||
from fastAutoTest.utils.vmhook import VMShutDownHandler, UncaughtExceptionHandler
|
||||
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
__all__ = {
|
||||
"WxDriver"
|
||||
}
|
||||
|
||||
|
||||
class WxDriver(object):
|
||||
def initDriver(self):
|
||||
if self._hasInit:
|
||||
return
|
||||
|
||||
self._urlFetcher = WxWebSocketDebugUrlFetcher(device=self._device)
|
||||
url = self._urlFetcher.fetchWebSocketDebugUrl()
|
||||
|
||||
dirPath = os.path.split(os.path.realpath(__file__))[0]
|
||||
PLUG_SRC = os.path.join(dirPath, 'apk', 'inputPlug.apk')
|
||||
if not AdbHelper.hasApkInstalled(packageName='com.tencent.fat.wxinputplug'):
|
||||
AdbHelper.installApk(PLUG_SRC, device=self._device, installOverride=True)
|
||||
self.logger.info('install ---> ' + PLUG_SRC)
|
||||
self._vmShutdownHandler.registerToVM()
|
||||
UncaughtExceptionHandler.init()
|
||||
UncaughtExceptionHandler.registerUncaughtExceptionCallback(self._onUncaughtException)
|
||||
|
||||
self._selectDevice()
|
||||
|
||||
self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
|
||||
self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer, self._executor, self)
|
||||
self._networkHandler.connect()
|
||||
self._executor.put(self._networkHandler.receive)
|
||||
|
||||
self.initPageDisplayData()
|
||||
self._hasInit = True
|
||||
|
||||
# 为了输入中文要先安装一个插件,./wx/apk/inputPlug.apk
|
||||
def __init__(self, device=None):
|
||||
self._device = device
|
||||
self._urlFetcher = None
|
||||
self._webSocketDataTransfer = None
|
||||
self._vmShutdownHandler = VMShutDownHandler()
|
||||
self._networkHandler = None
|
||||
self._executor = SingleThreadExecutor()
|
||||
self.logger = Log().getLogger()
|
||||
|
||||
self._pageOperator = WxPageOperator()
|
||||
|
||||
self._hasInit = False
|
||||
|
||||
self.d = uiautomator.Device(self._device)
|
||||
self.html = None
|
||||
|
||||
def getDriverType(self):
|
||||
return constant.WXDRIVER
|
||||
|
||||
def initPageDisplayData(self):
|
||||
driverInfo = self.d.info
|
||||
displayDp = driverInfo.get('displaySizeDpY')
|
||||
displayPx = driverInfo.get('displayHeight')
|
||||
self.scale = (displayPx - 0.5) / displayDp
|
||||
windowHeight = self.getWindowHeight()
|
||||
self.appTitleHeight = displayDp - windowHeight
|
||||
|
||||
def changeDp2Px(self, xDp, yDp):
|
||||
xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale, self.appTitleHeight)
|
||||
return xPx, yPx
|
||||
|
||||
def needSwitchNextPage(self):
|
||||
return self._urlFetcher.needSwitchNextPage()
|
||||
|
||||
def _selectDevice(self):
|
||||
devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
|
||||
# 假如没有指定设备,那么判断当前机器是否连接1个设备
|
||||
if self._device is None:
|
||||
devicesCount = len(devicesList)
|
||||
errorMsg = None
|
||||
|
||||
if devicesCount <= 0:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_DEVICE_NOT_CONNECT)
|
||||
|
||||
elif devicesCount == 1:
|
||||
self._device = devicesList[0]
|
||||
|
||||
else:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_MULTIPLE_DEVICE)
|
||||
|
||||
if errorMsg is not None:
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
def switchToNextPage(self):
|
||||
"""
|
||||
需要重新获取当前页面的websocket的url
|
||||
"""
|
||||
self._networkHandler.disconnect()
|
||||
url = self._urlFetcher.fetchWebSocketDebugUrl(refetch=True)
|
||||
self.logger.debug('url -> ' + url)
|
||||
self._webSocketDataTransfer.setUrl(url)
|
||||
self._networkHandler.connect()
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def returnLastPage(self):
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
self.logger.info('')
|
||||
self._networkHandler.disconnect()
|
||||
self.switchToNextPage()
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
self._networkHandler.disconnect()
|
||||
self.d.press('back')
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
self.switchToNextPage()
|
||||
|
||||
def getDocument(self):
|
||||
"""
|
||||
获得getHtml中需要的nodeId
|
||||
在调用getHtml之前必须先调用这个方法
|
||||
"""
|
||||
sendStr = self._pageOperator.getDocument()
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def getHtml(self, nodeId=1):
|
||||
"""
|
||||
获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
|
||||
:param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
|
||||
"""
|
||||
self.logger.info('')
|
||||
self.switchToNextPage()
|
||||
self.getDocument()
|
||||
sendStr = self._pageOperator.getHtml(nodeId)
|
||||
self.html = self._networkHandler.send(sendStr).getResponse()[0]['result']['outerHTML']
|
||||
return self.html
|
||||
|
||||
def isElementExist(self, xpath):
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath)
|
||||
response = self._networkHandler.send(getExistCmd).getResponse()
|
||||
self.logger.debug(response)
|
||||
resultValueDict = response[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
num = 0
|
||||
while resultType == 'null' and num < 3:
|
||||
self.wait(WAIT_REFLESH_3_SECOND)
|
||||
self.switchToNextPage()
|
||||
getExistCmd = self._pageOperator.isElementExist(xpath)
|
||||
resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
|
||||
resultType = resultValueDict['result']['result']['subtype']
|
||||
num = num + 1
|
||||
self.logger.debug('isElementExist Response --> ' + str(resultValueDict))
|
||||
return resultType != 'null'
|
||||
|
||||
def getElementTextByXpath(self, xpath):
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
self.switchToNextPage()
|
||||
if self.isElementExist(xpath):
|
||||
getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getTextCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementSrcByXpath(self, xpath):
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
self.switchToNextPage()
|
||||
if self.isElementExist(xpath):
|
||||
getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
|
||||
resultValueDict = self._networkHandler.send(getSrcCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
|
||||
else:
|
||||
resultValue = None
|
||||
return resultValue
|
||||
|
||||
def getElementByXpath(self, xpath):
|
||||
"""
|
||||
:param:目标的xpath
|
||||
:return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
|
||||
例如:可以使用element.get("attrs")来拿到属性的数据
|
||||
可以用element.text拿到它的文字
|
||||
当element中含有列表时,使用for循环读取每一个item
|
||||
"""
|
||||
time.sleep(WAIT_REFLESH_1_SECOND)
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
htmlData = self.getHtml()
|
||||
if htmlData is not None:
|
||||
html = etree.HTML(htmlData)
|
||||
elementList = html.xpath(xpath)
|
||||
if len(elementList) != 0:
|
||||
return elementList[0]
|
||||
else:
|
||||
self.logger.info('找不到xpath为: ' + xpath + ' 的控件')
|
||||
return ''
|
||||
else:
|
||||
self.logger.info('获取到的html为空')
|
||||
return ''
|
||||
|
||||
def textElementByXpath(self, xpath, text, needClick=False):
|
||||
"""
|
||||
:param needClick:如果为true,会先对控件进行一次点击,以获得焦点
|
||||
|
||||
输入前会先保存当前默认的输入法
|
||||
然后将输入法切换到adb插件
|
||||
输入中文,输入后讲输入法转换回默认输入发
|
||||
needClick代表是否需要先对xpath的控件进行一次点击
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
|
||||
if needClick:
|
||||
self.clickElementByXpath(xpath, byUiAutomator=True)
|
||||
|
||||
if self.isElementExist(xpath):
|
||||
ADB_SHELL = 'adb shell '
|
||||
self.logger.debug('textElementByXpath xpath exist')
|
||||
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_DEFAULT_INPUT_METHOD = ADB_SHELL + 'settings get secure default_input_method'
|
||||
SET_INPUT_METHOD = ADB_SHELL + 'ime set {0}'
|
||||
CHINESE_INPUT_METHOD = 'com.tencent.fat.wxinputplug/.XCXIME'
|
||||
INPUT_TEXT = ADB_SHELL + "am broadcast -a INPUT_TEXT --es TEXT '{0}'"
|
||||
|
||||
DEFAULT_INPUT_METHOD = commandHelper.runCommand(GET_DEFAULT_INPUT_METHOD)[0].replace("\r\n", " ")
|
||||
osName = OS.getPlatform()
|
||||
INPUT_TEXT_CMD = {
|
||||
"Darwin": INPUT_TEXT.format(text),
|
||||
"Windows": INPUT_TEXT.format(text).decode('utf-8').replace(u'\xa0', u' ').encode('GBK'),
|
||||
"Linux": INPUT_TEXT.format(text)
|
||||
}
|
||||
|
||||
self.logger.debug(SET_INPUT_METHOD.format(CHINESE_INPUT_METHOD))
|
||||
commandHelper.runCommand(SET_INPUT_METHOD.format(CHINESE_INPUT_METHOD))
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
self.logger.debug(INPUT_TEXT_CMD.get(osName))
|
||||
commandHelper.runCommand(INPUT_TEXT_CMD.get(osName))
|
||||
self.logger.debug(SET_INPUT_METHOD.format(DEFAULT_INPUT_METHOD))
|
||||
commandHelper.runCommand(SET_INPUT_METHOD.format(DEFAULT_INPUT_METHOD))
|
||||
self.wait(WAIT_REFLESH_05_SECOND)
|
||||
|
||||
def clickElementByXpath(self, xpath, visibleItemXpath=None, byUiAutomator=False):
|
||||
"""
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
if self.isElementExist(xpath):
|
||||
self.scrollToElementByXpath(xpath, visibleItemXpath)
|
||||
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
x = self._getRelativeDirectionValue('x')
|
||||
y = self._getRelativeDirectionValue('y')
|
||||
self.logger.debug('clickElementByXpath x:' + str(x) + ' y:' + str(y))
|
||||
|
||||
if not byUiAutomator:
|
||||
clickCommand = self._pageOperator.clickElementByXpath(x, y)
|
||||
return self._networkHandler.send(clickCommand)
|
||||
else:
|
||||
xPx, yPx = self.changeDp2Px(x, y)
|
||||
self.d.click(xPx, yPx)
|
||||
|
||||
def clickFirstElementByText(self, text, visibleItemXpath=None, byUiAutomator=False):
|
||||
"""
|
||||
通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
|
||||
"""
|
||||
self.clickElementByXpath('.//*[text()="' + text + '"]', visibleItemXpath, byUiAutomator)
|
||||
|
||||
def getElementCoordinateByXpath(self, elementXpath):
|
||||
'''
|
||||
获得Element的坐标
|
||||
:param elementXpath:待获取坐标的元素的xpath
|
||||
:return:element相对于整个屏幕的x、y坐标,单位为px
|
||||
'''
|
||||
self.logger.info(
|
||||
'elementXpathXpath ---> ' + elementXpath)
|
||||
if self.isElementExist(elementXpath):
|
||||
sendStr = self._pageOperator.getElementRect(elementXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
x = self._getRelativeDirectionValue('x')
|
||||
y = self._getRelativeDirectionValue('y')
|
||||
xPx, yPx = self.changeDp2Px(x, y)
|
||||
return xPx, yPx
|
||||
|
||||
def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=600):
|
||||
"""
|
||||
默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
|
||||
:param xpath: 要滑动到屏幕中控件的xpath
|
||||
:param visibleItemXpath: container中当前可见的一个xpath
|
||||
:return:
|
||||
"""
|
||||
self.logger.info('xpath ---> ' + xpath)
|
||||
sendStr = self._pageOperator.getElementRect(xpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
top = self._getRelativeDirectionValue('topp')
|
||||
bottom = self._getRelativeDirectionValue('bottom')
|
||||
left = self._getRelativeDirectionValue('left')
|
||||
right = self._getRelativeDirectionValue('right')
|
||||
self.logger.debug('scrollToElementByXpath -> top:bottom:left:right = ' + str(top) + " :" + str(bottom) \
|
||||
+ " :" + str(left) + " :" + str(right))
|
||||
|
||||
if visibleItemXpath is None:
|
||||
endTop = 0
|
||||
endLeft = 0
|
||||
endBottom = self.getWindowHeight()
|
||||
endRight = self.getWindowWidth()
|
||||
else:
|
||||
sendStr = self._pageOperator.getElementRect(visibleItemXpath)
|
||||
self._networkHandler.send(sendStr)
|
||||
|
||||
endTop = self._getRelativeDirectionValue('topp')
|
||||
endBottom = self._getRelativeDirectionValue('bottom')
|
||||
endLeft = self._getRelativeDirectionValue('left')
|
||||
endRight = self._getRelativeDirectionValue('right')
|
||||
self.logger.debug(
|
||||
'scrollToElementByXpath -> toendTop:endBottom:endLeft:endRight = ' + str(endTop) + " :" + str(
|
||||
endBottom) \
|
||||
+ " :" + str(endLeft) + " :" + str(endRight))
|
||||
|
||||
'''
|
||||
竖直方向的滑动
|
||||
'''
|
||||
if endBottom < bottom:
|
||||
scrollYDistance = endBottom - bottom
|
||||
elif top < 0:
|
||||
scrollYDistance = -(top - endTop)
|
||||
else:
|
||||
scrollYDistance = 0
|
||||
|
||||
if scrollYDistance < 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance - 80,
|
||||
speed)
|
||||
elif scrollYDistance > 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance + 80,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('y方向不需要滑动')
|
||||
'''
|
||||
水平方向的滑动
|
||||
'''
|
||||
if right > endRight:
|
||||
scrollXDistance = endRight - right
|
||||
elif left < 0:
|
||||
scrollXDistance = -(left - endLeft)
|
||||
else:
|
||||
scrollXDistance = 0
|
||||
|
||||
if scrollXDistance != 0:
|
||||
self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), scrollXDistance, 0,
|
||||
speed)
|
||||
else:
|
||||
self.logger.debug('x方向不需要滑动')
|
||||
|
||||
def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
|
||||
self.logger.info('')
|
||||
sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance, speed)
|
||||
return self._networkHandler.send(sendStr)
|
||||
|
||||
def _getRelativeDirectionValue(self, directionKey='topp'):
|
||||
'''
|
||||
获取相关的方向数据参数值
|
||||
:param directionKey: key值
|
||||
:return:
|
||||
'''
|
||||
directionCommand = self._pageOperator.getJSValue(directionKey)
|
||||
directionResponse = self._networkHandler.send(directionCommand).getResponse()[0]
|
||||
directionValue = directionResponse['result']['result']['value']
|
||||
return directionValue
|
||||
|
||||
'''
|
||||
性能数据
|
||||
'''
|
||||
|
||||
def getMemoryInfo(self):
|
||||
'''
|
||||
获得小程序进程占用内存信息
|
||||
:return: 小程序进程占用内存信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:appbrand0'
|
||||
return commandHelper.runCommand(GET_MEMORY_INFO)
|
||||
|
||||
def getCPUInfo(self):
|
||||
'''
|
||||
获得小程序进程占用CPU信息
|
||||
:return: 小程序进程占用CPU信息
|
||||
'''
|
||||
self.logger.info('')
|
||||
ADB_SHELL = 'adb shell '
|
||||
if self._device:
|
||||
ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
|
||||
GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:appbrand0'
|
||||
return commandHelper.runCommand(GET_CPU_INFO)
|
||||
|
||||
def getPageHeight(self):
|
||||
getPageHeightCmd = self._pageOperator.getPageHeight()
|
||||
resultValueDict = self._networkHandler.send(getPageHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getWindowWidth(self):
|
||||
'''
|
||||
:return:手机屏幕的宽度
|
||||
'''
|
||||
getWindowWidthCmd = self._pageOperator.getWindowWidth()
|
||||
resultValueDict = self._networkHandler.send(getWindowWidthCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def getWindowHeight(self):
|
||||
'''
|
||||
:return:手机屏幕的高度
|
||||
'''
|
||||
getWindowHeightCmd = self._pageOperator.getWindowHeight()
|
||||
resultValueDict = self._networkHandler.send(getWindowHeightCmd).getResponse()[0]
|
||||
resultValue = resultValueDict['result']['result']['value']
|
||||
return resultValue
|
||||
|
||||
def wait(self, seconds):
|
||||
time.sleep(seconds)
|
||||
|
||||
def addShutDownHook(self, func, *args, **kwargs):
|
||||
'''
|
||||
添加当程序正常关闭时的操作,可以进行一些环境清理操作等
|
||||
'''
|
||||
self._vmShutdownHandler.registerVMShutDownCallback(func, *args, **kwargs)
|
||||
|
||||
def _onUncaughtException(self, exctype, value, tb):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._networkHandler is not None:
|
||||
self._networkHandler.quit()
|
||||
|
||||
if self._executor is not None:
|
||||
self._executor.shutDown()
|
||||
|
||||
UncaughtExceptionHandler.removeHook()
|
||||
|
||||
def setDebugLogMode(self):
|
||||
'''
|
||||
开启debug的日志模式
|
||||
'''
|
||||
Log().setDebugLogMode()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
|
@ -0,0 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from fastAutoTest.core.common.command.commandProcessor import CommandProcessor
|
||||
from fastAutoTest.core.wx import wxUserAPI
|
||||
|
||||
|
||||
class WxPageOperator(object):
|
||||
processor = CommandProcessor('wx')
|
||||
|
||||
def clickElementByXpath(self, x, y):
|
||||
params = {"x": x, "y": y}
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.CLICK, **params)
|
||||
|
||||
def getJSValue(self, value):
|
||||
params = {"value": value}
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.GET_JS_VALUE, **params)
|
||||
|
||||
def getElementRect(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(wxUserAPI.ByType.XPATH, wxUserAPI.ActionType.GET_ELEMENT_RECT,
|
||||
**params)
|
||||
|
||||
def returnLastPage(self):
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.RETURN_LAST_PAGE)
|
||||
|
||||
def getPageHeight(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
wxUserAPI.ActionType.GET_PAGE_HEIGHT)
|
||||
|
||||
def isElementExist(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(wxUserAPI.ByType.XPATH, wxUserAPI.ActionType.IS_ELEMENT_EXIST,
|
||||
**params)
|
||||
|
||||
def getElementTextByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(wxUserAPI.ByType.XPATH, wxUserAPI.ActionType.GET_ELEMENT_TEXT,
|
||||
**params)
|
||||
|
||||
def getElementSrcByXpath(self, xpath):
|
||||
params = {"xpath": xpath}
|
||||
return self.processor.doCommandWithElement(wxUserAPI.ByType.XPATH, wxUserAPI.ActionType.GET_ELEMENT_SRC,
|
||||
**params)
|
||||
|
||||
def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
|
||||
params = {"x": x, "y": y, "xDistance": xDistance, "yDistance": yDistance, "speed": speed}
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.SCROLL, **params)
|
||||
|
||||
def getWindowHeight(self):
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.GET_WINDOW_HEIGHT)
|
||||
|
||||
def getWindowWidth(self):
|
||||
return self.processor.doCommandWithoutElement(wxUserAPI.ActionType.GET_WINDOW_WIDTH)
|
||||
|
||||
def getDocument(self):
|
||||
return self.processor.doCommandWithoutElement(
|
||||
wxUserAPI.ActionType.GET_DOCUMENT)
|
||||
|
||||
def getHtml(self, nodeId):
|
||||
params = {"nodeId": nodeId}
|
||||
return self.processor.doCommandWithoutElement(
|
||||
wxUserAPI.ActionType.GET_HTML, **params)
|
||||
|
||||
def changeDp2Px(self, xDp, yDp, scale, appTitleHeight):
|
||||
xPx = int(xDp * scale + 0.5)
|
||||
yPx = int((yDp + appTitleHeight) * scale + 0.5)
|
||||
return xPx, yPx
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class ByType(object):
|
||||
ID = "id"
|
||||
NAME = "name"
|
||||
XPATH = "xpath"
|
||||
|
||||
|
||||
class ActionType(object):
|
||||
CLICK = "click"
|
||||
TEXT = "text"
|
||||
SCROLL = "scroll"
|
||||
RETURN_LAST_PAGE = "returnPage"
|
||||
CLOSE_WINDOW = "closeWindow"
|
||||
GET_PICKER_RECT = "getRect"
|
||||
GET_ELEMENT_TEXT = "getElementText"
|
||||
GET_ELEMENT_SRC = "getElementSrc"
|
||||
GET_PAGE_HEIGHT = 'getPageHeight'
|
||||
GET_ELEMENT_RECT = 'getElementRect'
|
||||
GET_JS_VALUE = 'getJsValue'
|
||||
IS_ELEMENT_EXIST = 'isElementExist'
|
||||
GET_WINDOW_HEIGHT = 'getWindowHeight'
|
||||
GET_WINDOW_WIDTH = 'getWindowWidth'
|
||||
GET_HTML = "getHTML"
|
||||
GET_DOCUMENT = "getDocument"
|
|
@ -0,0 +1,341 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
import urllib2
|
||||
|
||||
import bidict
|
||||
from websocket import create_connection
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import *
|
||||
from fastAutoTest.utils.adbHelper import AdbHelper
|
||||
from fastAutoTest.utils.commandHelper import runCommand
|
||||
from fastAutoTest.utils.common import OS
|
||||
from fastAutoTest.utils.logger import Log
|
||||
|
||||
_ADB_GET_TOP_ACTIVITY_CMD = {
|
||||
"Darwin": "adb shell dumpsys activity top | grep ACTIVITY", # Mac os 下查找字符串是grep
|
||||
"Linux": "adb shell dumpsys activity top | grep ACTIVITY", # Mac os 下查找字符串是grep
|
||||
"Windows": "adb shell dumpsys activity top | findstr ACTIVITY" # windows 下查找字符串是findstr
|
||||
}
|
||||
|
||||
_ADB_GET_WEBVIEW_TOOLS_CMD = {
|
||||
"Darwin": "adb shell cat /proc/net/unix | grep webview_devtools_remote_%s", # Mac os 下查找字符串是grep
|
||||
"Linux": "adb shell cat /proc/net/unix | grep webview_devtools_remote_%s", # Mac os 下查找字符串是grep
|
||||
"Windows": "adb shell cat /proc/net/unix | findstr webview_devtools_remote_%s" # windows 下查找字符串是findstr
|
||||
}
|
||||
|
||||
MODE_NORMAL = 0
|
||||
MODE_EMBEDDED_H5 = 1
|
||||
|
||||
|
||||
class WxWebSocketDebugUrlFetcher(object):
|
||||
DEFAULT_LOCAL_FORWARD_PORT = 9223
|
||||
# 根据dict中存储的数量来获得新添加页面的名称
|
||||
pageMap = {1: 'event',
|
||||
2: 'first',
|
||||
3: 'second',
|
||||
4: 'third',
|
||||
5: 'fourth',
|
||||
6: 'fifth'}
|
||||
|
||||
def __init__(self, device, localForwardPort=None):
|
||||
self._device = device
|
||||
self._localForwardPort = localForwardPort if localForwardPort is not None \
|
||||
else WxWebSocketDebugUrlFetcher.DEFAULT_LOCAL_FORWARD_PORT
|
||||
self._webSocketDebugUrl = None
|
||||
# 通过dic来管理页面
|
||||
self.pageUrlDict = bidict.bidict()
|
||||
self.logger = Log().getLogger()
|
||||
self.mode = MODE_NORMAL
|
||||
|
||||
def fetchWebSocketDebugUrl(self, refetch=False):
|
||||
# 如果没有获取或者需要重新获取debug url,那么获取一次,否则直接返回
|
||||
if self._webSocketDebugUrl is None:
|
||||
self._fetchInner()
|
||||
if refetch:
|
||||
self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(self._localForwardPort)
|
||||
return self._webSocketDebugUrl
|
||||
|
||||
def getDevice(self):
|
||||
return self._device
|
||||
|
||||
def getForwardPort(self):
|
||||
return self._localForwardPort
|
||||
|
||||
def _fetchInner(self):
|
||||
# 先获取微信appbrand进程Pid
|
||||
pid = WxWebSocketDebugUrlFetcher._fetchWeixinToolsProcessPid(device=self._device)
|
||||
|
||||
# 重定向端口
|
||||
WxWebSocketDebugUrlFetcher._forwardLocalPort(self._localForwardPort, pid, device=self._device)
|
||||
|
||||
# 获取本地http://localhost:{重定向端口}/json返回的json数据,提取里面的webSocketDebuggerUrl字段值
|
||||
self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(self._localForwardPort)
|
||||
|
||||
@staticmethod
|
||||
def _fetchWeixinToolsProcessPid(device=None):
|
||||
osName = OS.getPlatform()
|
||||
cmd = _ADB_GET_TOP_ACTIVITY_CMD[osName]
|
||||
stdout, stdError = runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
strlist = stdout.split('pid=')
|
||||
pid = strlist[1].split("\r\n")[0]
|
||||
webviewCmd = _ADB_GET_WEBVIEW_TOOLS_CMD[osName] % (pid)
|
||||
|
||||
# 验证是否启动了小程序webview
|
||||
try:
|
||||
webStdout, webStdError = runCommand(AdbHelper.specifyDeviceOnCmd(webviewCmd, device))
|
||||
except:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENTER_XCX)
|
||||
raise RuntimeError(errorMsg)
|
||||
return pid
|
||||
|
||||
@staticmethod
|
||||
def _forwardLocalPort(localPort, pid, device=None):
|
||||
cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (localPort, pid)
|
||||
runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))
|
||||
|
||||
def _fetchWebSocketDebugUrl(self, localPort):
|
||||
import time
|
||||
localUrl = "http://localhost:%s/json" % localPort
|
||||
errorMsg = None
|
||||
resultUrl = None
|
||||
|
||||
# 去掉代理
|
||||
urllib2.getproxies = lambda: {}
|
||||
|
||||
try:
|
||||
nums = 0
|
||||
while None == resultUrl and nums < 8:
|
||||
response = urllib2.urlopen(localUrl)
|
||||
responseJson = json.loads(response.read())
|
||||
if len(responseJson) != 1:
|
||||
|
||||
self._cleanJsonData(responseJson)
|
||||
|
||||
resultUrl = self._handleAndReturnWebSocketDebugUrl(responseJson)
|
||||
self.logger.debug('websocket --> ' + resultUrl)
|
||||
return resultUrl
|
||||
else:
|
||||
nums = nums + 1
|
||||
self.logger.debug('retry fetch num ---> ' + str(nums))
|
||||
time.sleep(1)
|
||||
|
||||
except IndexError:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENABLE_DEBUG_MODE)
|
||||
except LookupError:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_ENTER_XCX)
|
||||
except AttributeError:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_GET_XCX_PAGE_INFO)
|
||||
except Exception:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_CONFIG_PROXY)
|
||||
raise RuntimeError(errorMsg)
|
||||
|
||||
# 去掉file://开头的脏数据
|
||||
def _cleanJsonData(self, responseJson):
|
||||
removeList = []
|
||||
for response in responseJson:
|
||||
if u'file:///' in response['url']:
|
||||
removeList.append(response)
|
||||
for i in removeList:
|
||||
responseJson.remove(i)
|
||||
|
||||
def _handleAndReturnWebSocketDebugUrl(self, responseJson):
|
||||
responseLength = len(responseJson)
|
||||
pageUrlDictLength = len(self.pageUrlDict)
|
||||
# 如果第一次进入小程序,当前加载的dict为空
|
||||
if pageUrlDictLength == 0:
|
||||
# 考虑正常的小程序,有两个链接,一个是空的server,一个是首页面。
|
||||
if responseLength == 2:
|
||||
self.mode = MODE_NORMAL
|
||||
return self._initWebSocketUrlWithTwoPage(responseJson)
|
||||
# 如果是小程序内嵌H5的情况,有三个链接,其中两个ServiceWeChat的URL,一个H5的URL。
|
||||
if responseLength == 3:
|
||||
self.mode = MODE_EMBEDDED_H5
|
||||
return self._initWebSocketUrlWithThreePage(responseJson)
|
||||
|
||||
# 如果返回的大于当前打开的,说明要开启新的页面
|
||||
# 有两种情况,一种是打开正常的小程序页面,启动一个新链接,另一种是打开嵌套H5的小程序页面,会启动两个链接
|
||||
elif responseLength > pageUrlDictLength:
|
||||
if responseLength - pageUrlDictLength == 1:
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if self.pageUrlDict.inv.get(message) is None:
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength + 1): message})
|
||||
return responseJson[i]["webSocketDebuggerUrl"]
|
||||
elif responseLength - pageUrlDictLength == 2:
|
||||
h5Url = ''
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if self.pageUrlDict.inv.get(message) is None:
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength + 1): message})
|
||||
if 'https://servicewechat.com/' not in responseJson[i]["url"]:
|
||||
h5Url = responseJson[i]["webSocketDebuggerUrl"]
|
||||
return h5Url
|
||||
else:
|
||||
raise AttributeError()
|
||||
|
||||
# 如果返回的小于当前打开的,说明要删除一个页面
|
||||
# 有两种情况,一种是关闭正常的小程序页面,删除一个链接,另一种是关闭嵌套H5的小程序页面,需要删除两个链接
|
||||
elif responseLength < pageUrlDictLength:
|
||||
if pageUrlDictLength - responseLength == 1:
|
||||
sencondLastPageMessage = self.pageUrlDict.get(self.pageMap.get(pageUrlDictLength - 1))
|
||||
shouldReturnUrl = None
|
||||
del self.pageUrlDict[self.pageMap.get(pageUrlDictLength)]
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if message == sencondLastPageMessage:
|
||||
shouldReturnUrl = responseJson[i]["webSocketDebuggerUrl"]
|
||||
return shouldReturnUrl
|
||||
elif pageUrlDictLength - responseLength == 2:
|
||||
sencondLastPageMessage = self.pageUrlDict.get(self.pageMap.get(pageUrlDictLength - 2))
|
||||
shouldReturnUrl = None
|
||||
del self.pageUrlDict[self.pageMap.get(pageUrlDictLength)]
|
||||
del self.pageUrlDict[self.pageMap.get(pageUrlDictLength - 1)]
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if message == sencondLastPageMessage:
|
||||
shouldReturnUrl = responseJson[i]["webSocketDebuggerUrl"]
|
||||
return shouldReturnUrl
|
||||
|
||||
|
||||
# 如果返回的等于当前打开的,有两种情况。一种是两个URL时,返回最后一个webSocketUrl。一种是当有三个URL,小程序内嵌H5 URL时,返回内嵌的H5 URL。
|
||||
elif responseLength == pageUrlDictLength:
|
||||
if self.mode == MODE_NORMAL:
|
||||
lastPageMessage = self.pageUrlDict.get(self.pageMap.get(pageUrlDictLength))
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if message == lastPageMessage:
|
||||
return responseJson[i]["webSocketDebuggerUrl"]
|
||||
# 如果都为空,则在当前页面进行了跳转,因此要更新dict中的特征
|
||||
# 依次连接返回的所有page的websocketurl,找到一个更新后的页面
|
||||
for response in responseJson:
|
||||
websocketUrl = response["webSocketDebuggerUrl"]
|
||||
pageFeature = self._getPageFeature(websocketUrl)
|
||||
|
||||
if self.pageUrlDict.inv.get(pageFeature) is None:
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength): pageFeature})
|
||||
return websocketUrl
|
||||
else:
|
||||
return self.pageUrlDict.get(1)
|
||||
|
||||
def _initWebSocketUrlWithTwoPage(self, responseJson):
|
||||
eventData = u'[object Text][object HTMLDivElement][object Text]'
|
||||
responseLength = 2
|
||||
pageUrlDictLength = len(self.pageUrlDict)
|
||||
for i in range(responseLength):
|
||||
message = self._getPageFeature(responseJson[i]["webSocketDebuggerUrl"])
|
||||
if message == eventData:
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength + 1): message})
|
||||
if i == 0:
|
||||
firstMessage = self._getPageFeature(responseJson[1]["webSocketDebuggerUrl"])
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength + 2): firstMessage})
|
||||
return responseJson[1]["webSocketDebuggerUrl"]
|
||||
else:
|
||||
firstMessage = self._getPageFeature(responseJson[0]["webSocketDebuggerUrl"])
|
||||
self.pageUrlDict.update({self.pageMap.get(pageUrlDictLength + 2): firstMessage})
|
||||
return responseJson[0]["webSocketDebuggerUrl"]
|
||||
|
||||
def _initWebSocketUrlWithThreePage(self, responseJson):
|
||||
responseWesocketUrlDict = {}
|
||||
responseUrlList = []
|
||||
serviceUrlList = []
|
||||
h5Url = []
|
||||
responseLength = 3
|
||||
|
||||
for i in range(responseLength):
|
||||
response = responseJson[i]
|
||||
url = response['url']
|
||||
webSocketUrl = response['webSocketDebuggerUrl']
|
||||
responseUrlList.append(url)
|
||||
responseWesocketUrlDict[url] = webSocketUrl
|
||||
for url in responseUrlList:
|
||||
if u"servicewechat" in url:
|
||||
serviceUrlList.append(url)
|
||||
else:
|
||||
h5Url.append(url)
|
||||
if len(serviceUrlList) != 2 or len(h5Url) != 1:
|
||||
raise AttributeError()
|
||||
else:
|
||||
url = ''
|
||||
for url in serviceUrlList:
|
||||
webSocketUrl = responseWesocketUrlDict.get(url)
|
||||
print webSocketUrl
|
||||
webSocketConn = create_connection(url=webSocketUrl)
|
||||
webSocketConn.send(
|
||||
'{"id": 1,"method": "Runtime.evaluate","params": {"expression": "srcs = document.body.childNodes[0].getAttribute(\'src\')"}}'
|
||||
)
|
||||
results = webSocketConn.recv()
|
||||
result = json.loads(results)
|
||||
webSocketConn.close()
|
||||
if result.get('result').get('result').get('type') == u'string':
|
||||
url = result.get('result').get('result').get('value')
|
||||
break
|
||||
if h5Url[0] != url:
|
||||
raise AttributeError()
|
||||
else:
|
||||
self.pageUrlDict.update({1: responseWesocketUrlDict.get(h5Url[0])})
|
||||
self.pageUrlDict.update({2: responseWesocketUrlDict.get(serviceUrlList[0])})
|
||||
self.pageUrlDict.update({3: responseWesocketUrlDict.get(serviceUrlList[1])})
|
||||
return responseWesocketUrlDict.get(h5Url[0])
|
||||
|
||||
def _getPageFeature(self, url):
|
||||
import time
|
||||
retry = True
|
||||
nums = 0
|
||||
message = ''
|
||||
while (retry and nums < 8):
|
||||
try:
|
||||
webSocketConn = create_connection(url=url)
|
||||
|
||||
webSocketConn.send(
|
||||
'{"id": 1,"method": "Runtime.evaluate","params": {"expression": "var allNodeList = [];function getChildNode(node){var nodeList = node.childNodes;for(var i = 0;i < nodeList.length;i++){var childNode = nodeList[i];allNodeList.push(childNode);getChildNode(childNode);}};getChildNode(document.body);"}}'
|
||||
)
|
||||
webSocketConn.recv()
|
||||
webSocketConn.send(
|
||||
'{"id": 2,"method": "Runtime.evaluate","params": {"expression": "allNodeEle=\'\'"}}'
|
||||
)
|
||||
webSocketConn.recv()
|
||||
webSocketConn.send(
|
||||
'{"id": 3,"method": "Runtime.evaluate","params": {"expression": "for(j = 0; j < allNodeList.length; j++) {allNodeEle = allNodeEle+allNodeList[j];}"}}'
|
||||
)
|
||||
|
||||
results = webSocketConn.recv()
|
||||
|
||||
self.logger.debug(results)
|
||||
|
||||
result = json.loads(results)
|
||||
retry = True if result.get('result').get('wasThrown') is True or \
|
||||
result.get('result').get('result').get('value') == u'' or \
|
||||
result.get('result').get('result').get('type') == u'undefined' else False
|
||||
if retry:
|
||||
time.sleep(2)
|
||||
nums = nums + 1
|
||||
webSocketConn.close()
|
||||
Log().getLogger().info('retry getFeatur ---> ' + str(nums))
|
||||
continue
|
||||
else:
|
||||
message = result['result']['result']['value']
|
||||
retry = False
|
||||
webSocketConn.close()
|
||||
return message
|
||||
except:
|
||||
continue
|
||||
if retry or message == '':
|
||||
raise AttributeError()
|
||||
|
||||
def needSwitchNextPage(self):
|
||||
import json
|
||||
import urllib2
|
||||
localUrl = "http://localhost:9223/json"
|
||||
|
||||
# 去掉代理
|
||||
urllib2.getproxies = lambda: {}
|
||||
|
||||
response = urllib2.urlopen(localUrl)
|
||||
responseJson = json.loads(response.read())
|
||||
return len(responseJson) != len(self.pageUrlDict)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,123 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from commandHelper import runCommand
|
||||
|
||||
|
||||
class AdbHelper:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def specifyDeviceOnCmd(cmd, device):
|
||||
return cmd if device is None else cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
@staticmethod
|
||||
def listDevices(ignoreUnconnectedDevices=True, printDetails=False, cwd=None):
|
||||
import re
|
||||
|
||||
devicesList = []
|
||||
cmd = "adb devices"
|
||||
stdoutData, stderrorData = runCommand(cmd, printDetails=printDetails, cwd=cwd)
|
||||
lines = stdoutData.split(os.linesep)
|
||||
pattern = re.compile(r"\b(device|offline|emulator|host|unauthorized)\b")
|
||||
for line in lines:
|
||||
if pattern.findall(line):
|
||||
name, status = line.replace("\t", " ").strip().split()
|
||||
if ignoreUnconnectedDevices and status == "device":
|
||||
devicesList.append(name)
|
||||
else:
|
||||
devicesList.append(name)
|
||||
|
||||
return devicesList
|
||||
|
||||
@staticmethod
|
||||
def screenOn(device=None, printDetails=False, cwd=None):
|
||||
cmd = "adb shell input keyevent 224"
|
||||
if device:
|
||||
cmd = cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
return runCommand(cmd, printDetails=printDetails, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def unLockScreen(device=None, fromX=None, fromY=None, toX=None, toY=None, printDetails=False, cwd=None):
|
||||
|
||||
if not fromX:
|
||||
fromX = 300
|
||||
|
||||
if not fromY:
|
||||
fromY = 1000
|
||||
|
||||
if not toX:
|
||||
toX = 300
|
||||
|
||||
if not toY:
|
||||
toY = 500
|
||||
|
||||
cmd = "adb shell input swipe %s %s %s %s" % (
|
||||
fromX,
|
||||
fromY,
|
||||
toX,
|
||||
toY)
|
||||
|
||||
if device:
|
||||
cmd = cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
return runCommand(cmd, printDetails=printDetails, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def runInstrumentationTestForWholePackage(pkgName, device=None, runnerName=None, printDetails=False, cwd=None):
|
||||
if not runnerName:
|
||||
runnerName = "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
cmd = "adb shell am instrument -w -r -e package %s -e debug false %s.test/%s" % (
|
||||
pkgName,
|
||||
pkgName,
|
||||
runnerName)
|
||||
|
||||
if device:
|
||||
cmd = cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
return runCommand(cmd, printDetails=printDetails, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def runInstrumentationTestForClass(pkgName, testcaseName, device=None,
|
||||
runnerName=None, printDetails=False, cwd=None):
|
||||
if not runnerName:
|
||||
runnerName = "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
cmd = "adb shell am instrument -w -r -e debug false -e class %s.%s %s.test/%s" % (
|
||||
pkgName,
|
||||
testcaseName,
|
||||
pkgName,
|
||||
runnerName)
|
||||
|
||||
if device:
|
||||
cmd = cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
return runCommand(cmd, printDetails=printDetails, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def installApk(apkFile, device=None, installOverride=False, printDetails=False, cwd=None):
|
||||
cmd = "adb install {0}"
|
||||
|
||||
if device:
|
||||
cmd = cmd.replace("adb", "adb -s %s" % device)
|
||||
|
||||
if installOverride:
|
||||
cmd = cmd.replace("install", "install -r")
|
||||
|
||||
return runCommand(cmd.format(apkFile), printDetails=printDetails, cwd=cwd)
|
||||
|
||||
@staticmethod
|
||||
def hasApkInstalled(packageName='com.tencent.fat.wxinputplug', device=None):
|
||||
ADB_FIND_PACKAGE_CMD = 'adb shell pm list packages'
|
||||
if device:
|
||||
ADB_FIND_PACKAGE_CMD = ADB_FIND_PACKAGE_CMD.replace("adb", "adb -s %s" % device)
|
||||
stdout, stdError = runCommand(AdbHelper.specifyDeviceOnCmd(ADB_FIND_PACKAGE_CMD, device))
|
||||
return packageName in stdout
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class AtomicInteger(object):
|
||||
|
||||
def __init__(self, initValue=0):
|
||||
self._initValue = initValue
|
||||
self._lock = Lock()
|
||||
|
||||
def getAndIncrement(self):
|
||||
with self._lock:
|
||||
tmp = self._initValue
|
||||
self._initValue += 1
|
||||
return tmp
|
||||
|
||||
def incrementAndGet(self):
|
||||
with self._lock:
|
||||
self._initValue += 1
|
||||
return self._initValue
|
||||
|
||||
def getAndDecrement(self):
|
||||
with self._lock:
|
||||
tmp = self._initValue
|
||||
self._initValue -= 1
|
||||
return tmp
|
||||
|
||||
def decrementAndGet(self):
|
||||
with self._lock:
|
||||
self._initValue -= 1
|
||||
return self._initValue
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import subprocess
|
||||
|
||||
from fastAutoTest.core.common.errormsgmanager import *
|
||||
|
||||
|
||||
def runCommand(cmd, printDetails=False, cwd=None):
|
||||
p = subprocess.Popen(cmd, cwd=cwd,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
stdout, stdError = p.communicate()
|
||||
if printDetails:
|
||||
print("runCommand --> " + stdout)
|
||||
|
||||
if printDetails and stdError:
|
||||
print(stdError)
|
||||
|
||||
if p.returncode != 0:
|
||||
errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_NOT_CONFIG_ENV)
|
||||
raise RuntimeError("%s, %s" % (cmd, errorMsg))
|
||||
|
||||
return stdout, stdError
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (9222, 123)
|
||||
out, error = runCommand(cmd)
|
||||
print(error == "")
|
||||
print out
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import platform
|
||||
|
||||
|
||||
class FuncHelper(object):
|
||||
@staticmethod
|
||||
def isCallable(obj):
|
||||
return obj is not None and hasattr(obj, '__call__')
|
||||
|
||||
|
||||
class DictHelper(object):
|
||||
@staticmethod
|
||||
def getValue(dict, key, defaultValue=None):
|
||||
if key in dict:
|
||||
return dict[key]
|
||||
else:
|
||||
return defaultValue
|
||||
|
||||
|
||||
class OS(object):
|
||||
@staticmethod
|
||||
def getPlatform():
|
||||
return platform.system()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Drvier类型
|
||||
H5DRIVER = 'H5Driver'
|
||||
WXDRIVER = 'WxDriver'
|
||||
QQDRIVER = 'QQDriver'
|
||||
|
||||
# 等待刷新时间0.5s
|
||||
WAIT_REFLESH_05_SECOND = 0.5
|
||||
|
||||
# 1s
|
||||
WAIT_REFLESH_1_SECOND = 1
|
||||
|
||||
# 2s
|
||||
WAIT_REFLESH_2_SECOND = 2
|
||||
|
||||
# 3s
|
||||
WAIT_REFLESH_3_SECOND = 3
|
||||
|
||||
# 40s
|
||||
WAIT_REFLESH_40_SECOND = 40
|
||||
|
||||
# 60s
|
||||
WAIT_REFLESH_60_SECOND = 60
|
||||
|
||||
# 发送数据尝试总次数
|
||||
SEND_DATA_ATTEMPT_TOTAL_NUM = 7
|
||||
|
||||
# 发送数据尝试三次
|
||||
SEND_DATA_ATTEMPT_3 = 3
|
||||
|
||||
# 发送数据尝试五次
|
||||
SEND_DATA_ATTEMPT_5 = 5
|
|
@ -0,0 +1,30 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
class Log(object):
|
||||
NOTSET = logging.NOTSET
|
||||
DEBUG = logging.DEBUG
|
||||
INFO = logging.INFO
|
||||
WARNING = logging.WARNING
|
||||
ERROR = logging.ERROR
|
||||
CRITICAL = logging.CRITICAL
|
||||
|
||||
def __init__(self, name='default'):
|
||||
self.name = name
|
||||
self.logger = logging.getLogger(self.name)
|
||||
self.logger.setLevel(self.INFO)
|
||||
if not self.logger.handlers:
|
||||
format = logging.Formatter("%(asctime)-8s %(thread)d %(funcName)s %(message)s")
|
||||
consoleHandler = logging.StreamHandler(sys.stdout)
|
||||
consoleHandler.setFormatter(format)
|
||||
self.logger.addHandler(consoleHandler)
|
||||
|
||||
def setLevel(self, level):
|
||||
self.logger.setLevel(level)
|
||||
|
||||
def getLogger(self):
|
||||
return self.logger
|
||||
|
||||
def setDebugLogMode(self):
|
||||
self.logger.setLevel(self.DEBUG)
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import Queue
|
||||
import threading
|
||||
|
||||
from atomicinteger import AtomicInteger
|
||||
|
||||
__all__ = [
|
||||
"SingleThreadExecutor"
|
||||
]
|
||||
|
||||
|
||||
class _WorkerThread(threading.Thread):
|
||||
ID = AtomicInteger(initValue=0)
|
||||
|
||||
def __init__(self, queue):
|
||||
super(_WorkerThread, self).__init__(name="_WorkerThread_%s" % _WorkerThread.ID.getAndIncrement())
|
||||
self._queue = queue
|
||||
self._quit = False
|
||||
|
||||
def run(self):
|
||||
while not self._quit:
|
||||
try:
|
||||
inParam = self._queue.get(block=True)
|
||||
if inParam == self:
|
||||
# self means quit
|
||||
break
|
||||
|
||||
callbackFunc, args, kwargs = inParam
|
||||
if callable(callbackFunc):
|
||||
callbackFunc(*args, **kwargs)
|
||||
self._queue.task_done()
|
||||
except Exception as e:
|
||||
print(e.message)
|
||||
raise
|
||||
|
||||
def quit(self):
|
||||
if not self._quit:
|
||||
self._queue.put(self)
|
||||
self._quit = True
|
||||
|
||||
def isQuit(self):
|
||||
return self._quit
|
||||
|
||||
|
||||
class SingleThreadExecutor(object):
|
||||
def __init__(self):
|
||||
self._queue = Queue.Queue()
|
||||
self._workerThread = _WorkerThread(self._queue)
|
||||
self._workerThread.setDaemon(True)
|
||||
self._workerThread.start()
|
||||
|
||||
def put(self, func, *args, **kwargs):
|
||||
if self._workerThread.isQuit():
|
||||
raise RuntimeError("executor is quit")
|
||||
|
||||
self._queue.put((func, args, kwargs))
|
||||
|
||||
def shutDownNow(self):
|
||||
self._workerThread.quit()
|
||||
|
||||
def shutDown(self):
|
||||
self._queue.join()
|
||||
self.shutDownNow()
|
||||
self._workerThread.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import atexit
|
||||
import sys
|
||||
|
||||
|
||||
class VMShutDownHandler(object):
|
||||
"""
|
||||
虚拟机正常关闭时的回调接口,一般做一些环境清理工作
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._shutDownCallbacks = []
|
||||
|
||||
def registerToVM(self):
|
||||
atexit.register(self._handleVMShutDown)
|
||||
|
||||
def _handleVMShutDown(self):
|
||||
for func, args, kwargs in self._shutDownCallbacks:
|
||||
func(*args, **kwargs)
|
||||
|
||||
def registerVMShutDownCallback(self, func, *args, **kwargs):
|
||||
self._shutDownCallbacks.append((func, args, kwargs))
|
||||
|
||||
|
||||
class UncaughtExceptionHandler(object):
|
||||
"""
|
||||
虚拟机发生异常退出时的回调接口,一般做一些环境清理工作
|
||||
"""
|
||||
|
||||
_callbacks = []
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
sys.excepthook = UncaughtExceptionHandler._handleUncaughtException
|
||||
|
||||
@staticmethod
|
||||
def _handleUncaughtException(exctype, value, tb):
|
||||
for func in UncaughtExceptionHandler._callbacks:
|
||||
func(exctype, value, tb)
|
||||
raise Exception(exctype)
|
||||
|
||||
@staticmethod
|
||||
def registerUncaughtExceptionCallback(func):
|
||||
UncaughtExceptionHandler._callbacks.append(func)
|
||||
|
||||
@staticmethod
|
||||
def removeHook():
|
||||
sys.excepthook = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
|
@ -0,0 +1,14 @@
|
|||
# coding=utf-8
|
||||
from fastAutoTest.core.h5.h5Engine import H5Driver
|
||||
|
||||
# http://h5.baike.qq.com/mobile/enter.html 从微信进入此链接,首屏加载完后执行脚本
|
||||
if __name__ == '__main__':
|
||||
h5Driver = H5Driver()
|
||||
h5Driver.initDriver()
|
||||
h5Driver.clickElementByXpath('/html/body/div[1]/div/div[3]/p')
|
||||
h5Driver.clickFirstElementByText('白内障')
|
||||
h5Driver.returnLastPage()
|
||||
h5Driver.returnLastPage()
|
||||
print(h5Driver.getElementTextByXpath('/html/body/div[1]/div/div[3]/p'))
|
||||
h5Driver.close()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# coding=utf-8
|
||||
from fastAutoTest.core.qq.qqEngine import QQDriver
|
||||
|
||||
# 从动态 -> 动漫进入
|
||||
if __name__ == '__main__':
|
||||
qqDriver = QQDriver()
|
||||
qqDriver.initDriver()
|
||||
qqDriver.clickFirstElementByText('英雄救美,这也太浪漫了')
|
||||
qqDriver.returnLastPage()
|
||||
qqDriver.clickElementByXpath('//*[@id="app"]/div/ul/li[2]')
|
||||
qqDriver.returnLastPage()
|
||||
qqDriver.close()
|
|
@ -0,0 +1,18 @@
|
|||
# coding=utf-8
|
||||
from fastAutoTest.core.wx.wxEngine import WxDriver
|
||||
import os
|
||||
|
||||
# 进入企鹅医典小程序
|
||||
if __name__ == '__main__':
|
||||
wxDriver = WxDriver()
|
||||
wxDriver.initDriver()
|
||||
# 点击全部疾病
|
||||
wxDriver.clickElementByXpath('/html/body/div[1]/div/div[3]/p')
|
||||
wxDriver.clickFirstElementByText('白内障')
|
||||
wxDriver.returnLastPage()
|
||||
wxDriver.returnLastPage()
|
||||
# 截图
|
||||
dirPath = os.path.split(os.path.realpath(__file__))[0]
|
||||
PIC_SRC = os.path.join(dirPath, 'pic.png')
|
||||
wxDriver.d.screenshot(PIC_SRC)
|
||||
wxDriver.close()
|
|
@ -0,0 +1,29 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
NAME = "fastAutoTest"
|
||||
VERSION = "2.2.2"
|
||||
AUTHOR = "jaggergan,fitchzheng"
|
||||
PACKAGES = find_packages()
|
||||
CLASSIFIERS = [
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Unix",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
]
|
||||
|
||||
INSTALL_REQUIRES = [
|
||||
"websocket-client>=0.44.0",
|
||||
"uiautomator>=0.3.2",
|
||||
"lxml>=4.0.0",
|
||||
"bidict>=0.14.0"
|
||||
|
||||
]
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
author=AUTHOR,
|
||||
packages=PACKAGES,
|
||||
classifiers=CLASSIFIERS,
|
||||
include_package_data=True,
|
||||
install_requires=INSTALL_REQUIRES
|
||||
)
|