
Security News
AI Agent Lands PRs in Major OSS Projects, Targets Maintainers via Cold Outreach
An AI agent is merging PRs into major OSS projects and cold-emailing maintainers to drum up more work.
hmdriver2
Advanced tools
写这个项目前github上已有个叫
hmdriver的项目,但它是侵入式(需要提前在手机端安装一个testRunner app);另外鸿蒙官方提供的hypium自动化框架,使用较为复杂,依赖繁杂。于是决定重写一套。
hmdriver2 是一款支持HarmonyOS NEXT系统的UI自动化框架,无侵入式,提供应用管理,UI操作,元素定位等功能,轻量高效,上手简单,快速实现鸿蒙应用自动化测试需求。

微信交流群(永久生效)
HDC环境
hdc文件在command-line-tools/sdk/default/openharmony/toolchains目录下export HM_SDK_HOME="/Users/develop/command-line-tools/sdk/default" //请以sdk实际安装目录为准
export PATH=$PATH:$HM_SDK_HOME/hms/toolchains:$HM_SDK_HOME/openharmony/toolchains
export HDC_SERVER_PORT=7035
电脑插上手机,开启USB调试,确保执行hdc list targets 可以看到设备序列号
安装hmdirver2 基础库
pip3 install -U hmdriver2
如果需要使用屏幕录屏 功能,则需要安装额外依赖opencv-python
(由于opencv-python比较大,因此没有写入到主依赖中,按需安装)
pip3 install -U "hmdriver2[opencv-python]"
from hmdriver2.driver import Driver
d = Driver()
print(d.device_info)
# ouput: DeviceInfo(productName='HUAWEI Mate 60 Pro', model='ALN-AL00', sdkVersion='12', sysVersion='ALN-AL00 5.0.0.60(SP12DEVC00E61R4P9log)', cpuAbi='arm64-v8a', wlanIp='172.31.125.111', displaySize=(1260, 2720), displayRotation=<DisplayRotation.ROTATION_0: 0>)
d.start_app("com.kuaishou.hmapp")
d(text="精选").click()
d.swipe(0.5, 0.8, 0.5, 0.4)
...
UI 控件树可视化工具,查看控件树层级,获取控件详情。

详细介绍请看 ui-viewer
如何需要连接远端的HDC Server来实现操作远端设备执行自动化,运行脚本前需要设置环境变量
export HDC_SERVER_HOST=127.0.0.1 # Replace with the remote host
export HDC_SERVER_PORT=8710
PS 如果需要移除环境变量,执行以下命令
unset HDC_SERVER_HOST
unset HDC_SERVER_PORT
from hmdriver2.driver import Driver
d = Driver()
# d = Driver("FMR0223C13000649")
参数serial 通过hdc list targets 命令获取;如果不传serial,则默认读取hdc list targets的第一个设备
初始化driver后,下面所有的操作都是调用dirver实现
d.install_app("/Users/develop/harmony_prj/demo.hap")
d.uninstall_app("com.kuaishou.hmapp")
传入的参数是package_name,可通过hdc命令获取hdc shell bm dump -a
d.start_app("com.kuaishou.hmapp")
d.start_app("com.kuaishou.hmapp", "EntryAbility")
package_name, page_name分别为包名和ability name,可以通过hdc命令获取:hdc shell aa dump -l
不传page_name时,默认会使用main ability作为page_name
d.stop_app("com.kuaishou.hmapp")
d.clear_app("com.kuaishou.hmapp")
该方法表示清除App数据和缓存
d.get_app_info("com.kuaishou.hmapp")
输出的数据结构是Dict, 内容如下
{
"appId": "com.kuaishou.hmapp_BIS88rItfUAk+V9Y4WZp2HgIZ/JeOgvEBkwgB/YyrKiwrWhje9Xn2F6Q7WKFVM22RdIR4vFsG14A7ombgQmIIxU=",
"appIdentifier": "5765880207853819885",
"applicationInfo": {
...
"bundleName": "com.kuaishou.hmapp",
"codePath": "/data/app/el1/bundle/public/com.kuaishou.hmapp",
"compileSdkType": "HarmonyOS",
"compileSdkVersion": "4.1.0.73",
"cpuAbi": "arm64-v8a",
"deviceId": "PHONE-001",
...
"vendor": "快手",
"versionCode": 999999,
"versionName": "12.2.40"
},
"compatibleVersion": 40100011,
"cpuAbi": "",
"hapModuleInfos": [
...
],
"reqPermissions": [
"ohos.permission.ACCELEROMETER",
"ohos.permission.GET_NETWORK_INFO",
"ohos.permission.GET_WIFI_INFO",
"ohos.permission.INTERNET",
...
],
...
"vendor": "快手",
"versionCode": 999999,
"versionName": "12.2.40"
}
d.get_app_main_ability("com.kuaishou.hmapp")
输出的数据结构是Dict, 内容如下
{
"name": "EntryAbility",
"moduleName": "kwai",
"moduleMainAbility": "EntryAbility",
"mainModule": "kwai",
"isLauncherAbility": true,
"score": 2
}
from hmdriver2.proto import DeviceInfo
info: DeviceInfo = d.device_info
输入内容如下
DeviceInfo(productName='HUAWEI Mate 60 Pro', model='ALN-AL00', sdkVersion='12', sysVersion='ALN-AL00 5.0.0.60(SP12DEVC00E61R4P9log)', cpuAbi='arm64-v8a', wlanIp='172.31.125.111', displaySize=(1260, 2720), displayRotation=<DisplayRotation.ROTATION_0: 0>)
然后就可以获取你想要的值, 比如
info.productName
info.model
info.wlanIp
info.sdkVersion
info.sysVersion
info.cpuAbi
info.displaySize
info.displayRotation
w, h = d.display_size
# outout: (1260, 2720)
from hmdriver2.proto import DisplayRotation
rotation = d.display_rotation
# ouput: DisplayRotation.ROTATION_0
设备旋转状态包括:
ROTATION_0 = 0 # 未旋转
ROTATION_90 = 1 # 顺时针旋转90度
ROTATION_180 = 2 # 顺时针旋转180度
ROTATION_270 = 3 # 顺时针旋转270度
from hmdriver2.proto import DisplayRotation
# 旋转180度
d.set_display_rotation(DisplayRotation.ROTATION_180)
d.go_home()
d.go_back()
d.screen_on()
d.screen_off()
d.unlock()
from hmdriver2.proto import KeyCode
d.press_key(KeyCode.POWER)
详细的Key code请参考 harmony key code
data = d.shell("ls -l /data/local/tmp")
print(data.output)
这个方法等价于执行 hdc shell ls -l /data/local/tmp
Notes: HDC详细的命令解释参考:awesome-hdc
d.open_url("http://www.baidu.com")
d.open_url("kwai://myprofile")
# 将手机端文件下载到本地电脑
d.pull_file(rpath, lpath)
# 将本地电脑文件推送到手机端
d.push_file(lpath, rpath)
参数rpath表示手机端文件路径,lpath表示本地电脑文件路径
d.screenshot(path)
参数path表示截图保存在本地电脑的文件路径
方式一
# 开启录屏
d.screenrecord.start("test.mp4")
# do somethings
time.sleep(5)
# 结束录屏
d.screenrecord.stop()
上述方式如果录屏过程中,脚本出现异常时,stop无法被调用,导致资源泄漏,需要加上try catch
【推荐】方式二 ⭐️⭐️⭐️⭐️⭐️
with d.screenrecord.start("test2.mp4"):
# do somethings
time.sleep(5)
通过上下文语法,在录屏结束时框架会自动调用stop 清理资源
Notes: 使用屏幕录屏需要依赖opencv-python
pip3 install -U "hmdriver[opencv-python]"
d.click(x, y)
# eg.
d.click(200, 300)
d.click(0.4, 0.6)
参数x, y表示点击的坐标,可以为绝对坐标值,也可以为相当坐标(屏幕百分比)
d.double_click(x, y)
# eg.
d.double_click(500, 1000)
d.double_click(0.5, 0.4)
d.long_click(x, y)
# eg.
d.long_click(500, 1000)
d.long_click(0.5, 0.4)
d.swipe(x1, y1, x2, y2, spped)
# eg.
d.swipe(600, 2600, 600, 1200, speed=2000) # 上滑
d.swipe(0.5, 0.8, 0.5, 0.4, speed=2000)
x1, y1表示滑动的起始点,x2, y2表示滑动的终点speed为滑动速率, 范围:200~40000, 不在范围内设为默认值为2000, 单位: 像素点/秒
d.swipe_ext("up") # 向上滑动,"left", "right", "up", "down"
d.swipe_ext("right", scale=0.8) # 向右滑动,滑动距离为屏幕宽度的80%
d.swipe_ext("up", box=(0.2, 0.2, 0.8, 0.8)) # 在屏幕 (0.2, 0.2) -> (0.8, 0.8) 这个区域上滑
# 使用枚举作为参数
from hmdriver2.proto import SwipeDirection
d.swipe_ext(SwipeDirection.DOWN) # 向下滑动
direction表示滑动方向,可以为up, down, left, right, 也可以为SwipeDirection的枚举值scale表示滑动距离百分比,范围:0.1~1.0, 默认值为0.8box表示滑动区域,格式为(x1, y1, x2, y2), 表示滑动区域的左上角和右下角的坐标,可以为绝对坐标值,也可以为相当坐标(屏幕百分比)Notes: swipe_ext和swipe的区别在于swipe_ext可以指定滑动区域,并且可以指定滑动方向,更简洁灵活
复杂手势就是手指按下start,移动move,暂停pause的集合,最后运行action
g = d.gesture
g.start(x1, y1, interval=0.5)
g.move(x2, y2)
g.pause(interval=1)
g.move(x3, y3)
g.action()
也支持链式调用(推荐)
d.gesture.start(x1, y1, interval=.5).move(x2, y2).pause(interval=1).move(x3, y3).action()
参数x, y表示坐标位置,可以为绝对坐标值,也可以为相当坐标(屏幕百分比),interval表示手势持续的时间,单位秒。
如果只有start手势,则等价于点击:
d.gesture.start(x, y).action() # click
# 等价于
d.click(x, y)
如下是一个复杂手势的效果展示

d.input_text(text)
# eg.
d.input_text("adbcdfg")
参数x, y表示输入的位置,text表示输入的文本
控件查找支持这些by属性
idkeytexttypedescriptionclickablelongClickablescrollableenabledfocusedselectedcheckedcheckableisBeforeisAfterNotes: 获取控件属性值可以配合 UI inspector 工具查看
普通定位
d(text="tab_recrod")
d(id="drag")
# 定位所有`type`为Button的元素,选中第0个
d(type="Button", index=0)
Notes:当同一界面有多个属性相同的元素时,index属性非常实用
模糊定位TODO
组合定位
指定多个by属性进行元素定位
# 定位`type`为Button且`text`为tab_recrod的元素
d(type="Button", text="tab_recrod")
相对定位
# 定位`text`为showToast的元素的前面一个元素
d(text="showToast", isAfter=True)
# 定位`id`为drag的元素的后面一个元素
d(id="drag", isBefore=True)
结合上面讲的控件选择器,就可以进行元素的查找
d(text="tab_recrod").exists()
d(type="Button", text="tab_recrod").exists()
d(text="tab_recrod", isAfter=True).exists()
# 返回 True or False
d(text="tab_recrod").find_component()
# 当没找到返回None
d(text="tab_recrod").info
# output:
{
"id": "",
"key": "",
"type": "Button",
"text": "tab_recrod",
"description": "",
"isSelected": False,
"isChecked": False,
"isEnabled": True,
"isFocused": False,
"isCheckable": False,
"isClickable": True,
"isLongClickable": False,
"isScrollable": False,
"bounds": {
"left": 539,
"top": 1282,
"right": 832,
"bottom": 1412
},
"boundsCenter": {
"x": 685,
"y": 1347
}
}
也可以单独调用对应的属性
d(text="tab_recrod").id
d(text="tab_recrod").key
d(text="tab_recrod").type
d(text="tab_recrod").text
d(text="tab_recrod").description
d(text="tab_recrod").isSelected
d(text="tab_recrod").isChecked
d(text="tab_recrod").isEnabled
d(text="tab_recrod").isFocused
d(text="tab_recrod").isCheckable
d(text="tab_recrod").isClickable
d(text="tab_recrod").isLongClickable
d(text="tab_recrod").isScrollable
d(text="tab_recrod").bounds
d(text="tab_recrod").boundsCenter
d(type="Button").count # 输出当前页面`type`为Button的元素数量
# 也可以这样写
len(d(type="Button"))
d(text="tab_recrod").click()
d(type="Button", text="tab_recrod").click()
d(text="tab_recrod").click_if_exists()
以上两个方法有一定的区别
click 如果元素没找到,会报错ElementNotFoundErrorclick_if_exists 即使元素没有找到,也不会报错,相当于跳过d(text="tab_recrod").double_click()
d(type="Button", text="tab_recrod").double_click()
d(text="tab_recrod").long_click()
d(type="Button", text="tab_recrod").long_click()
from hmdriver2.proto import ComponentData
componentB: ComponentData = d(type="ListItem", index=1).find_component()
# 将元素拖动到元素B上
d(type="ListItem").drag_to(componentB)
drag_to的参数component为ComponentData类型
# 将元素按指定的比例进行捏合缩小1倍
d(text="tab_recrod").pinch_in(scale=0.5)
# 将元素按指定的比例进行捏合放大2倍
d(text="tab_recrod").pinch_out(scale=2)
其中scale参数为放大和缩小比例
d(text="tab_recrod").input_text("abc")
d(text="tab_recrod").clear_text()
xpath选择器基于标准的xpath规范,也可以使用//*[@属性="属性值"]的样式(xpath lite)
d.xpath('//root[1]/Row[1]/Column[1]/Row[1]/Button[3]')
d.xpath('//*[@text="showDialog"]')
控件xpath路径获取可以配合 UI inspector 工具查看
d.xpath('//*[@text="showDialog"]').exists() # 返回True/False
d.xpath('//root[1]/Row[1]/Column[1]/Row[1]/Button[3]').exists()
d.xpath('//*[@text="showDialog"]').click()
d.xpath('//root[1]/Row[1]/Column[1]/Row[1]/Button[3]').click_if_exists()
以上两个方法有一定的区别
click 如果元素没找到,会报错XmlElementNotFoundErrorclick_if_exists 即使元素没有找到,也不会报错,相当于跳过d.xpath('//*[@text="showDialog"]').double_click()
d.xpath('//*[@text="showDialog"]').long_click()
d.xpath('//*[@text="showDialog"]').input_text("adb")
d.dump_hierarchy()
输出控件树格式参考 hierarchy.json
# 启动toast监控
d.toast_watcher.start()
# do something 比如触发toast的操作
d(text="xx").click()
# 获取toast
toast = d.toast_watcher.get_toast()
# output: 'testMessage'
See DEVELOP.md
FAQs
UI Automation Framework for Harmony Next
We found that hmdriver2 demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
An AI agent is merging PRs into major OSS projects and cold-emailing maintainers to drum up more work.

Research
/Security News
Chrome extension CL Suite by @CLMasters neutralizes 2FA for Facebook and Meta Business accounts while exfiltrating Business Manager contact and analytics data.

Security News
After Matplotlib rejected an AI-written PR, the agent fired back with a blog post, igniting debate over AI contributions and maintainer burden.