赞
踩
https://www.cnblogs.com/Ronaldo-HD/p/9907747.html
众所周知,安卓单台设备的UI自动化测试已经比较完善了,有数不清的自动化框架或者工具。但是介绍多设备管理的内容并不多,当手里的手机多了之后,要做自动化测试平台,这块的东西又不得不碰,摆脱USB限制,接入WiFi,才能更加自由
浅谈自动化测试工具 python-uiautomator2 · TesterHome
windows下载安装adb(极其简单)_adb工具_x2584179909的博客
GitHub - openatx/uiautomator2: Android Uiautomator2 Python Wrapper
- # Since uiautomator2 is still under development, you have to add --pre to install the development version
- pip install --upgrade --pre uiautomator2
-
- # Or you can install directly from github source
- git clone https://github.com/openatx/uiautomator2
- pip install -e uiautomator2
atx-agent是运行在设备上的驻守程序,go开发,用于保活设备上相关的服务
usb连手机,手机的【开发者选项】里 打开允许【USB调试】
cmd 执行 adb devices,可以看到设备对应的 序列码,说明已连接上
cmd 执行 python -m uiautomator2 init。会在手机上安装两个APK:ATX-agent(小黄车)和 `com.github.uiautomator.test`(不可见)
这两个apk使用同一个证书签名的。 不可见的应用实际上是一个测试包,包含有所有的测试代码,核心的测试服务也是通过其启动的。 但是运行的时候,系统却需要那个小黄车一直在运行(在后台运行也可以)。一旦小黄车应用被杀,后台运行的测试服务也很快的会被杀掉。就算什么也不做,应用应用在后台,也会很快被系统回收掉。
至此已经可以用usb调试手机了,直接跳转到下面 安装weditor
如果想后续不用usb 而用wifi调试,还需要下面的步骤:
很多地方都是写 u2.connect('192.168.0.100'),尝试后报RuntimeError: USB device XXXXX is offline
首先,电脑和手机要在同一局域网内(连同一个wifi),此时电脑能ping到手机'192.168.0.100',但还不够。需要开启远程adb
参考下文
https://www.cnblogs.com/lxmtx/p/16071938.html
wifi连接adb需要tcpip连接模式,在数据线连接时就需要设定端口,使用如下命令(每台手机都需要这样设置一次,可以写个脚本批量设置)
cmd
>adb tcpip 5566
手机与电脑连接同一局域网,比如手机连接wifi后ip为 192.168.0.102,使用adb连接手机设备(手机ip在wlan上能找到、atx-agent里也有写)
cmd
>adb connect 192.168.0.102:5566
此时adb devices,就能看到一个局域网设备
后续才能用uiautomator2.connect()或weditor界面 操作手机
import uiautomator2 as u2 d = u2.connect('192.168.0.100:5566') d.app_current()
(如果是在weditor已打开的情况下配置的adb tcpip5566,则要重启weditor后才能成功connect '绿')
然后手机上的atx-agent才会启动。(未连接电脑前,atx-agent里面的uiautomator服务是不会启动的)
使用adb conncet数周后可能又连不上
>adb connect 192.168.2.80:5566 cannot connect to 192.168.2.80:5566: 由于目标计算机积极拒绝,无 法连接。 (10061)这时再接usb,即解决
>adb tcpip 5566
常用adb命令:
adb kill-server //结束adb服务 adb start-server //启动adb服务 adb devices //获取adb设备列表 adb connect xxx //创建与xxx的设备连接 connect to a device via TCP/IP [default port=5555] #如果已经kill-server,那么start-server和devices和connect的效果都是一样的,都能直接开启adb adb disconnect xxx //断开与xxx的设备连接 disconnect from given TCP/IP device [default port=5555], or all #当有其他进程正在使用这个连接时,断开将无效。如weditor依然在connect,或者cmd中还有d #kill-server也没用,weditor或cmd里一有操作就会再次启动adb [I 230717 19:47:55 page:204] Serial: android:192.168.0.100:5566 * daemon not running; starting now at tcp:5037 * daemon started successfully [W 230717 19:47:59 __init__:218] [pid:13800] atx-agent has something wrong, auto recovering更多adb问题见:Android adb网络连接Offline和 adb断开连接
更多的adb指令:adb help
https://www.jianshu.com/p/63c4d5c31909 >adb forward tcp:8888 tcp:9999 #执行完该命令后,转发PC机8888端口的数据到手机的9999端口 8888 >adb forward --list #查看一下转发是否成功,只有通过USB成功连接了手机该命令才能成功 35b14bdb tcp:8888 tcp:9999 #可以看到转发成功 >netstat -a |findstr 8888 #查看8888端口的状态 TCP 127.0.0.1:8888 DESKTOP-IB06ARQ:0 LISTENING #可以看到本地的8888端口是处于LISTENING状态 #确认了转发成功后,PC机作为Client端,手机作为Server端建立Socket连接,就可以进行通信了 >adb forward --remove tcp:8888 #在通信完毕后,停止转发 >adb forward --list #再次使用adb forward --list看不到连接就是移除成功。
今天看Connect to a device,又试了一下ip直连,不用adb wifi。发现可以执行
u2.connect('192.168.0.100')
但后续的代码都报USB device XXXXX is offline。。。什么鬼,成功仅限于执行上面这行
还是要先 adb connect 192.168.0.100:5566才行
- pip install --pre --upgrade weditor
-
- 出错就
-
- git clone https://github.com/openatx/weditor
- pip install -e weditor
cmd >python -m weditor (or 直接>weditor)
如果不能成功启动,则重新
uiautomator2 init
weditor
会打开一个浏览器页面localhost:17310
Android后面填 用adb devices得到的 序列号 或 ip
【connect】如果没有绿色树叶图标则未连接上。
【静态】和【实时】。【实时】则weditor的监视窗口随时与手机屏幕保持一致,但hierarchy刷新会比较慢;【静态】则需要手动点击【dump hierarchy】,监视窗口才会读取手机当前屏幕,hierarchy刷新比较快
weditor使用过程中,手机的ATX要保持运行(后台即可),保活uiautomator,这样weditor的连接才不会中断。
测试代码可以在weditor编辑窗口写,也可以在cmd->python进python shell写。在weditor的好处是,输入d就有api提示。还可以点击监视窗口查看控件信息
不用进python shell. github.com/README.md#command-line
- screenshot: 截图
- $ uiautomator2 screenshot screenshot.jpg
-
- current: 获取当前包名和activity
- $ uiautomator2 current
-
- uninstall: 卸载 #没试过
- $ uiautomator2 uninstall <package-name> # 卸载一个包
- $ uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包
- $ uiautomator2 uninstall --all # 全部卸载
-
- stop: 停止应用
- $ uiautomator2 stop com.example.app # 停止一个app
- $ uiautomator2 stop --all # 停止所有的app #慎用
随着版本升级,设置过期的配置时,会提示Deprecated,但是不会抛异常。
- >adb devices
- >adb connect 192.168.2.80:5566
- >weditor
- # coding: utf-8
- #
- import uiautomator2 as u2
-
- d = u2.connect("--serial-here--") # 只有一个设备时可以省略参数
- d = u2.connect() # 一个设备时
- d = u2.connect("10.1.2.3:5566") # 通过设备的IP连接(需要在同一局域网且设备上的atx-agent已经安装并启动)
-
- #Retrieve the device info
- d.info #Get basic information, return a dictionary
- d.device_info #Get detailed information
- d.info.get('screenOn')
-
- d.window_size() #截取屏幕尺寸 return (1080, 2340)
-
- d.wait_activity(".ApiDemos", timeout=10) #return True of False
- #10秒在手机当前页面找到activity就返回True. activity可以在weditor找到
-
- d.serial #192.168.2.80:5566 设备序列号 或 ip
- d.wlan_ip #192.168.2.80
New command timeout :若客户端在 设定时间内没有发出新命令, uiautomator 服务就会结束。
- d.set_new_command_timeout(300) # change to 5 minutes, unit seconds
- #默认3分钟
- d.app_current() # 获取前台应用的 packageName, activity, pid
- d.app_list_running() #查看所有正在运行的 app
- d.app_info("com.examples.demo") #查看 app 信息
-
- img = d.app_icon("com.tencent.mobileqq") # save app icon
- #img.save("icon.png") #这个我也不知道图片存到哪
- img.save("D:\username\icon.png") #如果出现SyntaxError: (unicode error),换个路径试试
-
- d.app_start('com.tencent.qqmusic') # 启动应用。不需要在当前页面寻找
- #d.app_start("com.example.hello_world", ".MainActivity")
- d.app_start("com.tencent.mobileqq",'.activity.SplashActivity')
- d.app_start("com.example.app", stop=True) # 启动应用前 先停止应用
-
- pid = d.app_wait("com.example.android") # 等待应用运行, return pid(int)
- if not pid:
- print("com.example.android is not running")
- else:
- print("com.example.android pid is %d" % pid)
-
- d.app_wait("com.example.android", front=True) # 等待应用前台运行
- d.app_wait("com.example.android", timeout=20.0) # 最长等待时间20s(默认)
-
- #相当于`am force-stop`,因此你可能丢失数据
- d.app_stop("com.example.app") # 停止应用。无论应用在前、后台都会被停止,但 无法杀掉后台记录。
- d.app_stop_all() #这条慎用。。。系统壁纸都给我重置了,输入法也要重设默认
- d.app_stop_all(excludes=['com.examples.demo']) # stop all app except for com.examples.demo
-
- d.app_clear("com.example.app") #清后台 equivalent to `pm clear`
-
- d.app_install('http://some-domain.com/some.apk') #安装应用
- #Open Scheme
- #等同于在cmd直接adb shell am start -a android.intent.action.VIEW -d "appname://appnamehost"
- d.open_url("https://www.baidu.com") #系统自带浏览器打开网页
- d.open_url("taobao://taobao.com") # open Taobao app
- d.open_url("appname://appnamehost")
- d.push('foo.txt的', '/sdcard/') #推送文件到手机里的 文件夹
- d.push('foo.txt的', '/sdcard/bar.txt') #推送 并重命名
- d.push('foo.sh', '/data/local/tmp/',mode = 0o755) # push and change file access mode
- # push fileobj
- with open("foo.txt", 'rb') as f:
- d.push(f, "/sdcard/")
-
- d.pull('/sdcard/tmp.txt', 'tmp.txt') #从设备中拉取文件:
- # FileNotFoundError will raise if the file is not found on the device
- Debug HTTP requests
- Trace HTTP requests and response to find out how it works.
- 跟踪 HTTP 请求和响应以了解其工作原理
-
- >>> d.debug=False #默认
- >>> d.info
- {'currentPackageName': 'com.android.systemui', 'displayHeight': 2182, 'displayRotation': 0, 'displaySizeDpX': 384, 'displaySizeDpY': 832, 'displayWidth': 1080, 'productName': 'OnePlus7Pro_CH', 'screenOn': False, 'sdkInt': 30, 'naturalOrientation': True}
-
- >>> d.debug = True #开启debug,会获得每次的http信息
- >>> d.info
- 22:51:54.997 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "6b3c790115b09ee646131b1fb1eb1f33", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:56116/jsonrpc/0'
- 22:51:56.534 Response (1536 ms) >>>
- {"jsonrpc":"2.0","id":"6b3c790115b09ee646131b1fb1eb1f33","result":{"currentPackageName":"com.android.systemui","displayHeight":2182,"displayRotation":0,"displaySizeDpX":384,"displaySizeDpY":832,"displayWidth":1080,"productName":"OnePlus7Pro_CH","screenOn":false,"sdkInt":30,"naturalOrientation":true}}
- <<< END
- {'currentPackageName': 'com.android.systemui', 'displayHeight': 2182, 'displayRotation': 0, 'displaySizeDpX': 384, 'displaySizeDpY': 832, 'displayWidth': 1080, 'productName': 'OnePlus7Pro_CH', 'screenOn': False, 'sdkInt': 30, 'naturalOrientation': True}
- '''
- Set default element wait time, unit seconds 设置元素查找等待时间(默认20s)
- This function will have influence on click, long_click, drag_to, get_text, set_text, clear_text, etc.
- '''
-
- d.implicitly_wait(10.0) # 也可以通过d.settings['wait_timeout'] = 10.0 修改
- d(text="Settings").click() # if Settings button not show in 10s, UiObjectNotFoundError will raised
-
- print("wait timeout", d.implicitly_wait()) # get default implicit wait
- d.set_clipboard('text', 'label') #有第一个参数就够了。就是要复制到剪贴板的内容
- d.clipboard #查看剪贴板的内容
-
- d.screen_on() #打开屏幕
- d.screen_off() #关闭屏幕
- d.unlock() #解锁屏幕,源码就是power键+右滑,所以有锁的手机并不能解锁
-
- d.press("back") # 模拟点击返回键
- d.press("home") # 模拟Home键
-
- '''以下功能不同手机有区别
- power 锁屏键
- volume_up
- volume_down
- volume_mute 直接静音
- 菜单键: menu 一加是到壁纸设置的界面
- left、right、up、down 方向键可以控制写大段落文字时的光标,也可以切换app内的界面,比如微信
- enter 回车
- delete ( or del) 等于 退格键backspace
- recent (recent apps) 查看后台软件
- camera 一加手机无反应
- def press(self, key: Union[int, str], meta=None): 源码在uiautomator2/__init__.py#L1152
- 即press能接受str或int, 还能接受int+meta键同时按下,meta就是功能键,alt之类的
- '''
- #int即软键盘(拨号、打字时弹出的软键盘)上的字符 对应的keycode
- #keycode查询:https://developer.android.com/reference/android/view/KeyEvent.html#META_ALT_ON
-
- d.press(0x07, 0x02) # press keycode 0x07('0') with ALT(0x02),只有在拨号时能输出0,打字时无效
- d.press(0x07) #拨号、打字时都能输出0
- #我也不知道加alt有什么用。。。? 可能是为全键盘手机考虑的吧
- #Gesture interaction with the device
- d.click(x, y)
- d.click(10, 20) #像素
- d.click(0.9, 0.1) #整个屏幕的占比
-
- d.double_click(x, y)
- d.double_click(x, y, 0.1) # default duration between two click is 0.1s
-
- d.long_click(x, y)
- d.long_click(x, y, 0.5) # long click 0.5s (default)
-
- d.swipe(sx, sy, ex, ey)
- d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
- d.swipe(10, 20, 80, 90) # 从(10, 20)滑动到(80, 90)
- #滑动解锁
- d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2)) #swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)
- d.swipe_points([(0.501, 0.333),(0.278, 0.437),(0.516, 0.556),(0.739, 0.448),(0.514, 0.448)], 0.2) #0.2s
-
- d.swipe_ext("right") # 手指右滑,4选1 "left", "right", "up", "down"
- d.swipe_ext("right", scale=0.9) # 默认0.9, 滑动距离为屏幕宽度的90%
- d.swipe_ext("right", box=(0, 0, 100, 100)) # 在 (0,0) -> (100, 100) 这个区域内做滑动
- # 实践发现上滑或下滑的时候,从中点开始滑动成功率会高一些
- d.swipe_ext("up", scale=0.8) # 代码会vkk???
-
- # 还可以使用Direction作为参数
- from uiautomator2 import Direction
- d.swipe_ext(Direction.FORWARD) # 页面下翻, 等价于 d.swipe_ext("up"), 只是更好理解
- d.swipe_ext(Direction.BACKWARD) # 页面上翻
- d.swipe_ext(Direction.HORIZ_FORWARD) # 页面水平右翻
- d.swipe_ext(Direction.HORIZ_BACKWARD) # 页面水平左翻
-
- #drag是对sxsy处的app进行拖动(如果sxsy刚好有app),拖动到exey位置;swipe只是单纯的滑动。
- d.drag(sx, sy, ex, ey) #同样支持百分比 和 像素
- d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)
-
- #Touch and drap (Beta)
- #这个接口属于比较底层的原始接口
- d.touch.down(10, 10) # 模拟按下
- time.sleep(.01) # down 和 move 之间的延迟,自己控制
- d.touch.move(15, 15) # 模拟移动
- d.touch.up() # 模拟抬起
- #滑动解锁 全程
- d.screen_on()
- d.swipe_ext("right", 0.6)
- d.swipe_ext("up", 0.6)
- d.swipe_points([(0.501, 0.333),(0.278, 0.437),(0.516, 0.556),(0.739, 0.448),(0.514, 0.448)], 0.2)
直接在小黄车 停止服务, 或者用下面的命令
- #停用UiAutomator的守护程序
- d.uiautomator.stop()
- #d.service("uiautomator").stop() #DeprecationWarning: Call to deprecated method service. (You should use d.uiautomator.start() instead)
- #这会关闭atx-agent,atx里刷新服务状态会看到{'running':False}
- #在weditor里connect或者任意操作,就能重启atx-agent并恢复uiautomator服务
-
- # d.uiautomator.start() # 启动
- # d.uiautomator.running() # 是否在运行
- #检索/设置设备方向
- orientation = d.orientation
- print(orientation)
-
- # set orientation and freeze rotation. 忘记上次用什么app实现转向的了
- # notes: setting "upsidedown" requires Android>=4.3.
- d.set_orientation('n') # or "natural "
- d.set_orientation("l") # or "left"
- d.set_orientation("r") # or "right"
- d.set_orientation("u") # or "upsidedown" (can not be set)
- #关闭、开启 屏幕自动跟随旋转
- # freeze rotation
- d.freeze_rotation() #关闭 旋转
- # un-freeze rotation
- d.freeze_rotation(False) #开启 旋转
- #截图,默认返回pillow格式的图片
- # take screenshot and save to a file on the computer, require Android>=4.2.
- d.screenshot("home.jpg")
-
- # get PIL.Image formatted images. Naturally, you need pillow installed first
- image = d.screenshot() # default format="pillow"
- image.save("home.jpg") # or home.png. Currently, only png and jpg are supported
-
- # get opencv formatted images. Naturally, you need numpy and cv2 installed first
- import cv2
- image = d.screenshot(format='opencv')
- cv2.imwrite('home.jpg', image)
-
- # get raw jpeg data
- imagebin = d.screenshot(format='raw')
- open("some.jpg", "wb").write(imagebin)
- #Dump UI hierarchy 转储 UI 层次结构
- # get the UI hierarchy dump content (unicoded).
- xml = d.dump_hierarchy()
- #下拉菜单
- d.open_notification() #只下拉出第一行
- d.open_quick_settings() #下拉出第一页
选择器是一种方便的机制,用于识别当前窗口中的特定 UI 对象
- #Select the object with text 'Clock' and its className is 'android.widget.TextView'
- d(text='Clock', className='android.widget.TextView')
- '''
- Selector supports below parameters. Refer to [UiSelector Java doc](http://developer.android.com/tools/help/uiautomator/UiSelector.html) for detailed information.
- * `text`, `textContains`, `textMatches`, `textStartsWith`
- * `className`, `classNameMatches`
- * `description`, `descriptionContains`, `descriptionMatches`, `descriptionStartsWith`
- * `checkable`, `checked`, `clickable`, `longClickable`
- * `scrollable`, `enabled`,`focusable`, `focused`, `selected`
- * `packageName`, `packageNameMatches`
- * `resourceId`, `resourceIdMatches`
- * `index`, `instance`
Children and siblings
- # get the children or grandchildren 后代元素
- d(className="android.widget.ListView").child(text="Bluetooth") #text一定要完全匹配
- d(className="android.view.ViewGroup").child(description="vip歌曲").click()
- # 多个参数 指定父元素和子元素
- d(className="android.widget.LinearLayout", resourceId="com.tencent.mm:id/kp3").child(text="朋友圈", className="android.widget.TextView").click()
-
- # 通过父 和 孙(后代),去找子
- d(父属性*n).child_by_text(孙(后代)的text, 子属性*n).click() #返回子
- d(className="android.widget.LinearLayout").child_by_text("我的快递", className="android.widget.RelativeLayout").click() #当页面有很多属性符合的父和子时,可能会找不到。 这时需要添加更多的属性
- d(className="android.widget.LinearLayout").child_by_text("我的快递", className="android.widget.RelativeLayout", resourceId="com.alipay.android.phone.openplatform:id/home_app_view").click() #给子添加resourceId属性,就能找到了
- d(className="android.widget.LinearLayout").child_by_text("消息盒子", className="android.widget.LinearLayout", resourceId="com.alipay.mobile.homefeeds:id/notification_msg_box").click() #同上
- #用setting和wechat的界面 测试child_by_text时,因为可用的 子属性少,总是返回页面上text对不上的元素,原因未查明?
- #网上别人uiautomator用法d().child_by_text().siblings()
- #我uiautomator2用会报错'str' object has no attribute 'clone'
- #有人提了issue,作者暂时没空修
-
- #还可以滚动屏幕来寻找 allow_scroll_search=True
- d(className="android.widget.ScrollView").child_by_text("工具箱", allow_scroll_search=True, className="android.widget.LinearLayout").click()
- #又出现了选中其他元素的情况。。。总感觉child_by_text不怎么好用
-
- # get siblings 同级元素
- d(text="Google").sibling(className="android.widget.ImageView")
- d(text="支付宝").sibling(description="时钟").click()
- d(description="K歌").sibling(description="下载").click()
-
- #relative positioning 用相对位置来找元素
- d(text="Wi‑Fi").right(className="android.widget.Switch").click() #left right up down
-
- #Multiple instances 当找到很多个元素时,指定要第几个
- d(text="Add new", instance=0) # 计数从0开始,要第1个
- d(className="android.widget.TextView")[2].click() #要第3个
- #元素相关的其他api
- # get the count of views with text "Add new" on current screen
- d(text="Add new").count
- len(d(text="Add new")) # same as count property ,不是列表,但 是可迭代对象
-
- # get the instance via index
- d(text="Add new")[0]
- d(text="Add new")[1]
- ...
-
- # iterator
- for view in d(text="Add new"):
- view.info # ...
- d(text="Settings").exists # True if exists, else False
- d.exists(text="Settings") # alias of above property.
- # advanced usage
- d(text="Settings").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)
-
- d(text="Settings").info #Retrieve the info of the specific UI object
-
- #Get/Set/Clear text of an editable field (e.g., EditText widgets)
- d(text="Settings").get_text() # get widget text 有text的元素都能获得
- d(className="android.widget.MultiAutoCompleteTextView").set_text("My text...") # set the text 这个不是app改名,在widget.Text...的地方才能输入
- d(className="android.widget.EditText")[0].clear_text() # 在widget.Text...的地方才能清除
-
- #对非widget.Text...的元素,在光标处输入
- d.set_fastinput_ime(True) # 切换成FastInputIME输入法
- d.send_keys("你好123abcEFG") # adb广播输入
- d.clear_text() # 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7)
- d.set_fastinput_ime(False) # 切换成 系统自带的输入法(不是自己另外装的)
-
-
- d.send_action("search") # 该函数可以使用的参数有 go search send next done previous
- #什么时候该使用这个函数呢?
- #有些时候在EditText中输入完内容之后,调用press("search") or press("enter")发现并没有什么反应。
- #这个时候就需要send_action函数了,这里用到了只有输入法才能用的IME_ACTION_CODE。
- #send_action先broadcast命令发送给输入法操作IME_ACTION_CODE,由输入法完成后续跟EditText的通信。
- x, y = d(text="Settings").center() #Get Widget center point
-
- im = d(text="支付宝").screenshot() #将整个元素框截下来
- im.save("D:\你的名字\settings.jpg") #不知道为什么保存到桌面会报错,明明都是全英
- d(text="Settings").click() # click on the center of the specific ui object
- #自定义点击元素的位置,从元素框的左上角开始,写比例会自动计算
- d(text="Settings").click(offset=(0.5, 0.5)) # Default center
- d(text="Settings").click(offset=(0, 0)) # click left-top
- d(text="Settings").click(offset=(1, 1)) # click right-bottom
-
- # wait element to appear for at most 10 seconds and then click
- d(text="Settings").click(timeout=10) #等10s超时后报错
- clicked = d(text='Skip').click_exists(timeout=10.0) #同上,区别在10s内有返回True,没有返回False,不报错
-
- # click until element gone, return bool 一直点,直到页面内没有这个元素,返回True,如果元素还在,返False
- is_gone = d(text="Skip").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0
- #点10次,每次间隔1s
-
- # wait until the ui object appears
- d(text="Settings").wait(timeout=3.0) # return bool The default timeout is 20s.
- # wait until the ui object gone
- d(text="Settings").wait_gone(timeout=1.0)
- # notes : drag can not be used for Android<4.3.
- # drag the UI object to a screen point (x, y), in 0.5 second
- d(text="Settings").drag_to(x, y, duration=0.5)
- # drag the UI object to (the center position of) another UI object, in 0.25 second
- d(text="Settings").drag_to(text="Clock", duration=0.25)
-
- #从元素的中心滑动到其边缘
- d(text="Settings").swipe("up", steps=20) # 1 steps is about 5ms 当steps过短时,就和滑动屏幕一个效果
- #up right left down
-
- #Two-point gesture from one point to another 两点操作,比如放大图片
- d(text="Settings").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))
- d(className="android.widget.FrameLayout").gesture((0.388, 0.591), (0.626, 0.624), (0.175, 0.533), (0.929, 0.745))
- #也有预设好的,
- # notes : pinch can not be set until Android 4.3.
- # from edge to center. here is "In" not "in"
- d(text="Settings").pinch_in(percent=100, steps=10) #缩小图片
- # from center to edge
- d(text="Settings").pinch_out() #放大图片
- #fling on the specific ui object(scrollable) 快速滑动fling
- # fling.forward(default)下.vertically(default)竖直方向
- d(scrollable=True).fling()
- d(scrollable=True).fling.horiz.forward() #水平方向若不能滑动,会触发点击
- d(scrollable=True).fling.vert.backward() #上
- d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)
- d(scrollable=True).fling.toEnd()
-
- # scroll.forward(default).vertically(default) 一页页滑动scroll
- d(scrollable=True).scroll(steps=10)
- d(scrollable=True).scroll.horiz.forward(steps=100)
- d(scrollable=True).scroll.vert.backward()
- d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)
- d(scrollable=True).scroll.toEnd()
- # scroll forward vertically until specific ui object appears
- d(scrollable=True).scroll.to(text="Google") #这里是向下滑动,底部一出现这个元素就会停止滑动
更精准的定位。在weditor可以找到每个控件的xpath
- d.xpath('//*[@text="私人FM"]').get().info # 获取控件信息
- # wait exists 10s
- d.xpath("//android.widget.TextView").wait(10.0)
- # find and click
- d.xpath("//*[@content-desc='分享']").click()
- # check exists
- if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
- print("exists")
- # get all text-view text, attrib and center point
- for elem in d.xpath("//android.widget.TextView").all():
- print("Text:", elem.text)
- # Dictionary eg:
- # {'index': '1', 'text': '999+', 'resource-id': 'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false', 'enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false', 'long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}
- print("Attrib:", elem.attrib)
- # Coordinate eg: (100, 200)
- print("Position:", elem.center())
- print(el.elem) # 输出lxml解析出来的Node
- print("bounds:", el.bounds) # output tuple: (left, top, right, bottom)
Session represent an app lifecycle. Can be used to start app, detect app crash 当操作时app闪退,会抛出SessionBrokenError.
- sess = d.session("com.example.app") # 启动应用并获取session (应用此前未启动)
- sess.close() # 停止应用
- sess.restart() # 冷启动应用(重新分配新进程)
- sess.running() # True or False # check if session is ok.
- '''
- 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
- 热启动:当启动应用时,后台已有该应用的进程(例:按home键回到桌面,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
- 来源 https://www.zhihu.com/question/39146027/answer/337659054
- '''
-
- # When app is still running
- sess(text="Music").click() # operation goes normal 正常运行 就啥也不返回
- # If app crash or quit
- sess(text="Music").click() # raise SessionBrokenError 闪退或者崩溃则报错
-
- #和with一起用
- with d.session("com.netease.cloudmusic") as sess:
- sess(text="Play").click()
-
- #Attach to the running app 附加session到已启动的应用
- # launch app if not running, skip launch if already running
- sess = d.session("com.netease.cloudmusic", attach=True)
- # raise SessionBrokenError if not running
- sess = d.session("com.netease.cloudmusic", attach=True, strict=True)
目前采用了后台运行了一个线程的方法(依赖threading库),然后每隔一段时间dump一次hierarchy,匹配到元素之后执行相应的操作。
- d.watcher.reset() # 停止并移除所有的监控,常用于初始化
-
- # 常用写法
- d.watcher.when("取消").click() #注册监控
- d.watcher.start() # 开始后台监控
- d.watcher.start(2.0) # 默认监控间隔2.0s
-
- d.watcher.stop() # 停止监控
-
- d.watcher.run() # 强制运行所有监控
-
- d.watcher.remove() # 移除所有的监控
-
- # 注册名为ANR的监控,当出现ANR和Force Close时,点击Force Close
- d.watcher("ANR").when(xpath="ANR").when("Force Close").click()
- d.watcher.remove("ANR")# 移除名为ANR的监控
-
- # 其他回调例子
- d.watcher.when("抢红包").press("back")
- d.watcher.when("//*[@text = 'Out of memory']").call(lambda d: d.shell('am force-stop com.im.qq'))
-
- d.xpath("继续").click() # 使用d.xpath检查元素的时候,会触发watcher(目前最多触发5次)
- # 在回调中调用不会再次触发watcher
- def click_callback(d: u2.Device):
- d.xpath("确定").click() # 在回调中调用不会再次触发watcher
- ctx = d.watch_context() #执行以后立刻开始监听
- ctx.when("取消").click() #注册
- ctx.wait_stable() # 开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定)
- ctx.stop() #要用stop才能停止监听
-
- #原作者推荐下面用法,不需要自己start和stop,见仁见智吧
- #目前的这个watch_context是用threading启动的,每2s检查一次 目前还只有click这一种触发操作
- with d.watch_context() as ctx:
- ctx.when("取消").click() #如果只有这两行,则会立即执行'取消'(如果当前页面有'取消'),没有监听的效果
- ctx.wait_stable()
- ctx.when("取消").call(lambda d: d.press("back")) #加上下面两行就会进行3个watch check等待,这期间切换到'取消'界面,才体现监听效果
-
- #作者内置 注册了一些监听
- with d.watch_context(builtin=True) as ctx:
- # 在已有的基础上增加
- ctx.when("@tb:id/jview_view").when('//*[@content-desc="图片"]').click()
- '''
- self.when("继续使用").click()
- self.when("移入管控").when("取消").click()
- self.when("^立即(下载|更新)").when("取消").click()
- self.when("同意").click()
- self.when("^(好的|确定)").click()
- self.when("继续安装").click()
- self.when("安装").click()
- self.when("Agree").click()
- self.when("ALLOW").click()
- '''
- d.HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间
-
- # 当设备掉线时,等待设备在线时长,仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
- d.WAIT_FOR_DEVICE_TIMEOUT = 70
-
- #其他的配置
- print(d.settings)
-
- d.settings['operation_delay'] = (.5, 1) # 配置点击前延时0.5s,点击后延时1s
-
- # 修改 要延迟生效的操作
- # 其中 double_click, long_click 都对应click
- d.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']
-
- d.settings['xpath_debug'] = True # 开启xpath插件的调试日志
- d.settings['wait_timeout'] = 20.0 # 默认控件等待时间(原生操作,xpath插件的等待时间)
这里没有使用手机中自带的screenrecord命令,是通过获取手机图片合成视频的方法,所以需要安装一些其他的依赖,如imageio, imageio-ffmpeg, numpy等 因为有些依赖比较大,推荐使用镜像安装。下面的命令可安装上述依赖。
pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
有的依赖好像已经停止维护了,
ImportError: cannot import name 'create_connection' from 'websocket'
同上,就到这里吧
更多的api见
github.com/openatx/uiautomator2/README.md?plain=1#L850
Python+uiautomator2手机UI自动化测试实战 -- 2. 用法介绍_Ricky_Frog
十分钟弄懂最快的APP自动化工具uiautomator2(入门到精通) - 知乎
d.implicitly_wait(等待时长) #单位是s
sx, ex和sy,ey分别表示起点和终点的坐标 d.swipe(sx, sy, ex, ey, 0.5)
获取屏幕尺寸:d.window_size()
滑动距离尽量大点,比如x轴起点终点可以设置系数分别为0.9, 0.1,如果你设置为0.9,0.5,否则可能会出现滑动距离太小,导致没有滑过去的情况
停止app并清理环境
一个完整的用例就写完了,当然为了演示起见,我省略了很多,比如PO模式、pytest、日志、报告等等
原文见:uiautomator2/uiautomator2/ext /perf/
自动记录测试过程中的CPU,PSS, NET
- import uiautomator2 as u2
- import uiautomator2.ext.perf as perf
-
- package_name = "com.netease.cloudmusic"
- u2.plugin_register('perf', perf.Perf)
-
-
- def main():
- d = u2.connect()
- d.ext_perf.package_name = package_name
- d.ext_perf.csv_output = "perf.csv" # 保存数据到perf.csv
- # d.debug = True # 采集到数据就输出,默认关闭。如果后面接d.info之类的命令,会给出更多的数据
- # d.interval = 1.0 # 数据采集间隔,默认1.0s,尽量不要小于0.5s,因为采集内存比较费时间
- d.ext_perf.start()
-
- # run ... tests code here ...
- d.ext_perf.stop() # 最好结束的时候调用下,虽然不调用也没多大关系
-
- # generate images from csv
- # 需要安装 matplotlib, pandas, numpy, humanize
- d.ext_perf.csv2images()
-
-
- if __name__ == '__main__':
- main()
-
- '''
- csv2images函数更多的用法
- d.ext_perf.csv2images("perf.csv", target_dir="./")
- - PSS直接通过`dumpsys meminfo <package-name>`获取
- - CPU直接读取的`/proc/`下的文件计算出来的,多核的情况,数据是有可能超过100%的
- - rxBytes, txBytes 目前只有wlan的流量,tcp和udp的流量总和
- - fps 通过解析`dumpsys SurfaceFlinger --list` 和 `dumpsys SurfaceFlinger --latency <VIEW>` 计算出来
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。