commit of FAutoTest-version_2.2.2

This commit is contained in:
fitchzheng 2018-09-26 10:18:14 +08:00
commit a0a065bc7f
73 changed files with 4563 additions and 0 deletions

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
recursive-include fastAutoTest/core/wx/apk *.apk

210
README.md Normal file
View File

@ -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) 开源

BIN
dist/fastAutoTest-2.2.2-py2-none-any.whl vendored Normal file

Binary file not shown.

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

344
docs/APIS.md Normal file
View File

@ -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=NonebyUiautomator=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)**

38
docs/APPENDIX.md Normal file
View File

@ -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=响应idid只是用来区分当前请求对应的响应
> 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"/>.

50
docs/CONTRIBUTING.md Normal file
View File

@ -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
我们期待您通过PRPull 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)

22
docs/DEBUG.md Normal file
View File

@ -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"/>

32
docs/IFRAME.md Normal file
View File

@ -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"/>

31
docs/NOTICES.md Normal file
View File

@ -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*)

42
docs/QA.md Normal file
View File

@ -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)

Binary file not shown.

BIN
docs/images/CallProcess.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/images/chromeQuest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/images/contact.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/images/debug1.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

BIN
docs/images/debug2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

BIN
docs/images/debug3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
docs/images/h5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
docs/images/iframe1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
docs/images/iframe2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/images/iframe3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

BIN
docs/images/iframe4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
docs/images/iframe5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/images/iframe6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/images/iframe7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/images/iframe8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

0
fastAutoTest/__init__.py Normal file
View File

View File

View File

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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')

View File

@ -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

View File

View File

@ -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

View File

@ -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相对于整个屏幕的xy坐标单位为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相对于整个屏幕的xy坐标单位为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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

View File

@ -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相对于整个屏幕的xy坐标单位为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

View File

@ -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

View File

Binary file not shown.

View File

@ -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)

View File

@ -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相对于整个屏幕的xy坐标单位为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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

14
sample/H5Demo.py Normal file
View File

@ -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()

12
sample/QQDemo.py Normal file
View File

@ -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()

18
sample/XcqDemo.py Normal file
View File

@ -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
sample/__init__.py Normal file
View File

29
setup.py Normal file
View File

@ -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
)