├── README.md ├── base ├── __init__.py ├── action.py └── driver.py ├── run.py ├── tests ├── conftest.py └── test_case │ ├── test_search.yml │ └── test_top.yml └── utils ├── __init__.py ├── log.py └── shell.py /README.md: -------------------------------------------------------------------------------- 1 | Pytest 运行 Yaml 来驱动 Appium 进行 UI 测试 2 | 3 | Yaml 使用方式规则 4 | --- 5 | 6 | ```yaml 7 | test_index: 8 | - 9 | method: launchApp # 启动 APP 10 | - 11 | method: 方法名称 例如:click (必填) 12 | element: 查找元素id,class等 (选填,配合 method 如需要点击元素,查找元素等必填) 13 | type: 元素类型 id,xpath,class name,accessibility id (选填,会自动识别,如识别错误则自行填写) 14 | name: 测试步骤的名称 例如:点击搜索按钮 (选填) 15 | text: 需要输入或者查找的文本 (选填,配合部分 method 使用) 16 | time: 查找该元素需要的时间,默认 5s (选填) 17 | index: 页面有多个id,class时,不为空则查找元素数组下标 (选填) 18 | is_displayed: 默认 True ,当为 False 时元素未找到也不会抛异常(选填) 19 | ``` 20 | ``` 21 | 需要参数的 method 22 | | click(self, locator) 23 | | 基础的点击事件 24 | | is_element_displayed(self, locator) 25 | | 控件是否显示e 26 | | get_text(self, locator) 27 | | 获取元素文本 28 | | screenshot_element(self, locator) 29 | | 区域截图 30 | | set_text(self, locator) 31 | | 输入文本 32 | 33 | 不需要参数的 method 34 | | launchApp(self) 35 | | 重启应用程序 36 | | photograph(self) 37 | | 拍照 38 | | set_keycode_enter(self) 39 | | 回车键 40 | | set_keycode_search(self) 41 | | 搜索键 42 | | swip_down(self) 43 | | 向下滑动,常用于下拉刷新 44 | | swip_left(self) 45 | | 向左滑动 46 | | swip_right(self) 47 | | 向右滑动 48 | | swip_up(self) 49 | | 向上刷新 50 | | click_shoot_windows(self) 51 | | 检测权限窗口 52 | ``` 53 | #### 运行方式 54 | > pytest -s ./test_case/test_ranking.yml --alluredir './report/test' 55 | 56 | 或者直接运行文件目录 57 | 58 | 使用方法和基本 pytest 用法没有太大区别 59 | > pytest -s ./test_case --alluredir './report/test' -------------------------------------------------------------------------------- /base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/YamlAppium/32f3618107c2ff038bb68cd3a0dea18888cf33ff/base/__init__.py -------------------------------------------------------------------------------- /base/action.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @ Author:YueC 4 | @ Description:Appium api 封装层 5 | """ 6 | 7 | import time 8 | import allure 9 | from appium import webdriver 10 | from appium.webdriver.common.touch_action import TouchAction 11 | from selenium.webdriver.common.by import By 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | 14 | from utils import L 15 | 16 | 17 | def Ldict(element, type, name=None, text=None, time=5, index=0): 18 | """ 19 | :param element: 查找元素的名称 例如:xxx:id/xx 20 | :param type: 元素类型 id,xpath,class name,accessibility id 21 | :param name: 测试步骤的名称 22 | :param : 需要输入文本的名称 23 | :param time: 查找该元素需要的时间,默认 5s 24 | :param index: 不为空则查找元素数组下标 25 | :return: 26 | """ 27 | return {'element': element, 'type': type, 'name': name, 'text': text, 'time': time, 'index': index} 28 | 29 | 30 | class NotFoundElementError(Exception): 31 | pass 32 | 33 | 34 | class NotFoundTextError(Exception): 35 | pass 36 | 37 | 38 | class ElementActions: 39 | def __init__(self, driver: webdriver.Remote, adb=None, Parameterdict=None): 40 | self.Parameterdict = Parameterdict 41 | self.driver = driver 42 | self.ADB = adb 43 | if Parameterdict: 44 | self.width = self.driver.get_window_size()['width'] 45 | self.height = self.driver.get_window_size()['height'] 46 | self.apppid = self.get_app_pid() 47 | 48 | def reset(self, driver: webdriver.Remote): 49 | """单例模式下,当driver变动的时候,需要重置一下driver 50 | 单例模式开启装饰器singleton 51 | 52 | Args: 53 | driver: driver 54 | 55 | """ 56 | self.driver = driver 57 | self.width = self.driver.get_window_size()['width'] 58 | self.height = self.driver.get_window_size()['height'] 59 | return self 60 | 61 | def adb_shell(self, command, args, includeStderr=False): 62 | """ 63 | appium --relaxed-security 方式启动 64 | adb_shell('ps',['|','grep','android']) 65 | 66 | :param command:命令 67 | :param args:参数 68 | :param includeStderr: 为 True 则抛异常 69 | :return: 70 | """ 71 | result = self.driver.execute_script('mobile: shell', { 72 | 'command': command, 73 | 'args': args, 74 | 'includeStderr': includeStderr, 75 | 'timeout': 5000 76 | }) 77 | return result['stdout'] 78 | 79 | def get_app_pid(self): 80 | """ 获取包名PID 进程 81 | :return: int PID 82 | """ 83 | result = self.ADB.shell('"ps | grep {0}"'.format(self.Parameterdict.get('appPackage'))) 84 | 85 | # result = self.adb_shell('ps', ['|', 'grep', self.Parameterdict.get('appPackage')]) 86 | if result: 87 | return result.split()[1] 88 | else: 89 | return False 90 | 91 | def set_keycode_search(self): 92 | """ 搜索键 93 | """ 94 | self._send_key_event('KEYCODE_SEARCH') 95 | 96 | def set_keycode_enter(self): 97 | """ 回车键 98 | """ 99 | self._send_key_event('KEYCODE_ENTER') 100 | 101 | def clear(self): 102 | self.driver.quit() 103 | 104 | def launchApp(self): 105 | """ 重启应用程序 106 | """ 107 | with allure.step("重启应用程序"): 108 | self.driver.launch_app() 109 | self.apppid = self.get_app_pid() 110 | 111 | def open_url(self, locator): 112 | self.driver.get(locator.get('text')) 113 | 114 | @staticmethod 115 | def sleep(s): 116 | if isinstance(s, dict): 117 | s = s['element'] 118 | return time.sleep(s) 119 | 120 | def back_press(self): 121 | L.i("系统按键返回上一页") 122 | self.sleep(1) 123 | self._send_key_event('KEYCODE_BACK') 124 | 125 | def dialog_ok(self, wait=5): 126 | locator = {'name': '对话框确认键', 'timeOutInSeconds': wait, 'type': 'id', 'value': 'android:id/button1'} 127 | self.click(locator) 128 | 129 | def screenshot_element(self, locator): 130 | # 元素区域截图 131 | element = self._find_element(locator) 132 | pngbyte = element.screenshot_as_png 133 | allure.attach(pngbyte, locator.get('name'), allure.attachment_type.PNG) 134 | 135 | def set_number_by_soft_keyboard(self, nums): 136 | """模仿键盘输入数字,主要用在输入取餐码类似场景 137 | 138 | Args: 139 | nums: 数字 140 | """ 141 | list_nums = list(nums) 142 | for num in list_nums: 143 | self._send_key_event('KEYCODE_NUM', num) 144 | 145 | def click(self, locator, count=1): 146 | """基础的点击事件 147 | :param locator: 定位器 148 | :param count: 点击次数 149 | """ 150 | if locator.get('index'): 151 | el = self._find_elements(locator)[locator['index']] 152 | else: 153 | el = self._find_element(locator) 154 | 155 | if count == 1: 156 | el.click() 157 | else: 158 | touch_action = TouchAction(self.driver) 159 | try: 160 | for x in range(count): 161 | touch_action.tap(el).perform() 162 | except: 163 | pass 164 | 165 | def get_text(self, locator): 166 | """获取元素中的text文本 167 | :param locator: 定位器 168 | """ 169 | L.i("[获取]元素 %s " % locator.get('name')) 170 | 171 | if locator.get('index'): 172 | el = self._find_elements(locator)[locator['index']] 173 | else: 174 | el = self._find_element(locator) 175 | 176 | return el.text 177 | 178 | def set_text(self, locator, clear_first=False, click_first=True): 179 | """ 输入文本 180 | :param locator: 定位器 181 | :param clear_first: 是否先清空原来文本 182 | :param click_first: 是否先点击选中 183 | """ 184 | value = locator.get('text') 185 | if click_first: 186 | self._find_element(locator).click() 187 | if clear_first: 188 | self._find_element(locator).clear() 189 | L.i("[输入]元素 %s " % value) 190 | with allure.step("输入元素:{0}".format(value)): 191 | self._find_element(locator).send_keys(value) 192 | 193 | def swipeElementUp(self, element): 194 | """ IOS专用 在元素内部滑动 195 | :param element: 以查找到的元素 196 | """ 197 | scrolldict = {'direction': 'left', 'element': element.id} 198 | self.driver.execute_script('mobile: swipe', scrolldict) 199 | 200 | def swip_down(self, count=1, method=None, speed=1000): 201 | """ 向下滑动,常用于下拉刷新 202 | :param count: 滑动次数 203 | :param method: 传入的方法 method(action) ,如果返回为True,则终止刷新 204 | :param speed: 滑动速度 ms 205 | """ 206 | if count == 1: 207 | self.driver.swipe(self.width / 2, self.height * 2 / 5, self.width / 2, self.height * 4 / 5, speed) 208 | self.sleep(1) 209 | else: 210 | for x in range(count): 211 | self.driver.swipe(self.width / 2, self.height * 2 / 5, self.width / 2, self.height * 4 / 5, speed) 212 | self.sleep(1) 213 | try: 214 | if method(self): 215 | break 216 | except: 217 | pass 218 | L.i("[滑动]向下刷新 ") 219 | 220 | def swip_up(self, count=1, method=None, speed=1000): 221 | """ 向上刷新 222 | :param count: 滑动次数 223 | :param method: 传入的方法 method(action) ,如果返回为True,则终止刷新 224 | :param speed: 滑动速度 ms 225 | :return: 226 | 227 | """ 228 | if count == 1: 229 | self.sleep(1) 230 | self.driver.swipe(self.width / 2, self.height * 3 / 4, self.width / 2, self.height / 4, speed) 231 | self.sleep(2) 232 | else: 233 | for x in range(count): 234 | self.driver.swipe(self.width / 2, self.height * 3 / 4, self.width / 2, self.height / 4, speed) 235 | self.sleep(2) 236 | try: 237 | if method(self): 238 | break 239 | except: 240 | pass 241 | L.i("[滑动]向上刷新 ") 242 | 243 | def swip_left(self, height=0.5, count=1, speed=1000): 244 | """ 向左滑动 245 | :param height: 高度满屏幕为1 246 | :param count: 滑动次数 247 | :param speed: 滑动速度 ms 248 | :return: 249 | """ 250 | for x in range(count): 251 | self.sleep(1) 252 | self.driver.swipe(self.width * 7 / 8, self.height * height, self.width / 8, self.height * height, speed) 253 | self.sleep(2) 254 | L.i("[滑动]向左滑动") 255 | 256 | def swip_right(self, height=0.5, count=1, speed=1000): 257 | """向右滑动 258 | :param height: 高度满屏幕为1 259 | :param count: 滑动次数 260 | :param speed: 滑动速度 ms 261 | :return: 262 | """ 263 | for x in range(count): 264 | self.sleep(1) 265 | self.driver.swipe(self.width / 8, self.height * height, self.width * 7 / 8, self.height * height, speed) 266 | self.sleep(2) 267 | L.i("[滑动]向右滑动 ") 268 | 269 | def is_element_displayed(self, locator, is_raise=False, element=True): 270 | """ :控件是否显示e 271 | :param locator: 定位器 272 | :param is_raise: 是否抛异常 273 | :param element: 274 | :returns: 275 | true: 显示 276 | false: 不显示 277 | """ 278 | try: 279 | return WebDriverWait(self.driver, 2).until( 280 | lambda driver: self._get_element_by_type(driver, locator), 281 | '查找元素{0}失败'.format(locator.get('name'))) if element else WebDriverWait(self.driver, 2).until( 282 | lambda driver: self._get_element_by_type(driver, locator, element_type=False), 283 | '查找元素{0}失败'.format(locator.get('name'))) 284 | 285 | except Exception as E: 286 | L.w("页面中未找到 %s " % locator) 287 | if is_raise: 288 | raise E 289 | else: 290 | return False 291 | 292 | # ======================= private ==================== 293 | 294 | def _find_element(self, locator, is_need_displayed=True): 295 | """ :单个元素,如果有多个返回第一个 296 | :param locator: 定位器 297 | :param is_need_displayed: 是否需要定位的元素必须展示 298 | :return: 299 | :raises:NotFoundElementError 300 | """ 301 | 302 | with allure.step("检查:'{0}'".format(locator.get('name'))): 303 | try: 304 | if is_need_displayed: 305 | return WebDriverWait(self.driver, locator['time']).until( 306 | lambda driver: self._get_element_by_type(driver, locator), '查找元素'.format(locator.get('name'))) 307 | 308 | except Exception as e: 309 | print(e) 310 | L.e("页面中未能找到 %s 元素" % locator) 311 | raise Exception("页面中未能找到 [%s]" % locator.get('name')) 312 | 313 | def _find_elements(self, locator): 314 | """ 查找多元素 315 | :param locator: 定位器 316 | :return: [] 317 | """ 318 | with allure.step("检查:'{0}'".format(locator.get('name'))): 319 | try: 320 | return WebDriverWait(self.driver, locator['time']).until( 321 | lambda driver: self._get_element_by_type(driver, locator, False)) 322 | except: 323 | L.w("[elements] 页面中未能找到 %s 元素" % locator) 324 | return [] 325 | 326 | @staticmethod 327 | def _get_element_by_type(driver, locator, element_type=True): 328 | """ 329 | :param driver: driver session 330 | :param locator: 定位器 331 | :param element_type: 查找元素类型 332 | true:单个元素 333 | false:多个元素 334 | :return: 单个元素 或 元素list 335 | """ 336 | element = locator['element'] 337 | ltype = locator['type'] 338 | 339 | return driver.find_element(ltype, element) if element_type else driver.find_elements(ltype, element) 340 | 341 | def _send_key_event(self, arg, num=0): 342 | """ 343 | 操作实体按键 344 | Code码:https://developer.android.com/reference/android/view/KeyEvent.2018-5-21-18 345 | Args: 346 | arg: event_list key 347 | num: KEYCODE_NUM 时用到对应数字 348 | 349 | """ 350 | event_list = {'KEYCODE_HOME': 3, 'KEYCODE_BACK': 4, 'KEYCODE_MENU': 82, 'KEYCODE_NUM': 8, 'KEYCODE_ENTER': 66, 351 | 'KEYCODE_SEARCH': 84} 352 | if arg == 'KEYCODE_NUM': 353 | self.driver.press_keycode(8 + int(num)) 354 | elif arg in event_list: 355 | self.driver.press_keycode(int(event_list[arg])) 356 | 357 | def _set_network(self, arg): 358 | """ 359 | :param arg:可用 Android设置网络模式飞行模式,wifi,移动网络 360 | :return: 361 | """ 362 | event_list = {'Nonetwork': 0, 'Airplane': 1, 'wifi': 2, 'network': 4, 'Allnetwork': 6} 363 | self.driver.set_network_connection(event_list[arg]) 364 | 365 | def photograph(self): 366 | """不同系统手机拍照+确认""" 367 | str = self.ADB.get_android_brand() 368 | if 'MI' in str: 369 | ''' 370 | MIUI 371 | ''' 372 | self.driver.press_keycode(27) 373 | self.click(Ldict('com.miui.gallery:id/ok', By.ID, "确认按钮")) 374 | elif 'vivo' in str: 375 | self.click(Ldict('com.android.camera:id/shutter_button', By.ID, '拍照按钮')) 376 | self.click(Ldict('com.android.camera:id/btn_done', By.ID, '确认按钮')) 377 | # 三星 378 | elif 'G9350' in str: 379 | self.driver.press_keycode(27) 380 | self.click(Ldict('com.sec.android.app.camera:id/okay', By.ID, '保存')) 381 | self.click(Ldict('com.sec.android.gallery3d:id/save', By.ID, '完成')) 382 | 383 | elif 'Samsung' in str: 384 | self.click(Ldict('com.android.camera2:id/shutter_button', By.ID, '拍照按钮')) 385 | self.click(Ldict('com.android.camera2:id/done_button', By.ID, '确认按钮')) 386 | 387 | elif 'honor' in str: 388 | self.click(Ldict('com.huawei.camera:id/shutter_button', By.ID, '拍照按钮')) 389 | self.click(Ldict('com.huawei.camera:id/btn_review_confirm', By.ID, '确认按钮')) 390 | 391 | elif 'nubia' in str: 392 | self.click(Ldict('com.android.camera:id/shutter_button', By.ID, '拍照按钮')) 393 | self.click(Ldict('com.android.camera:id/btn_done', By.ID, '确认按钮')) 394 | 395 | def click_shoot_windows(self): 396 | """ 397 | :return:检测权限窗口 398 | """ 399 | 400 | try: 401 | els = self._find_elements(Ldict('android.widget.Button', By.CLASS_NAME, '获取权限', 2)) 402 | for el in els: 403 | text1 = el.text 404 | if text1 == '允许': 405 | el.click() 406 | return True 407 | elif text1 == '始终允许': 408 | el.click() 409 | return True 410 | elif text1 == '确定': 411 | el.click() 412 | return True 413 | return False 414 | except: 415 | return False 416 | -------------------------------------------------------------------------------- /base/driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @ Author:YueC 4 | @ Description:driver 驱动 5 | """ 6 | 7 | from appium import webdriver 8 | from base.action import ElementActions 9 | from utils import shell 10 | from utils.shell import Device 11 | 12 | 13 | class Singleton(object): 14 | """单例模式 15 | """ 16 | Action = None 17 | 18 | def __new__(cls, *args, **kw): 19 | if not hasattr(cls, '_instance'): 20 | udid = Device.get_android_devices()[0] 21 | host = "http://localhost:4723/wd/hub" 22 | desired_caps = {'appActivity': '.SplashActivity', 23 | 'appPackage': 'com.sina.weibo', 24 | 'autoGrantPermissions': True, 25 | 'autoLaunch': False, 26 | 'automationName': 'UiAutomator2', 27 | 'deviceName': udid, 28 | 'noReset': True, 29 | 'platformName': 'Android', 30 | 'platformVersion': '9.0', 31 | 'udid': udid} 32 | 33 | driver = webdriver.Remote(host, desired_caps) 34 | ADB = shell.ADB(udid) 35 | desired_caps['platformVersion'] = ADB.get_android_version() 36 | Action = ElementActions(driver, ADB, Parameterdict=desired_caps) 37 | orig = super(Singleton, cls) 38 | cls._instance = orig.__new__(cls, *args, **kw) 39 | cls._instance.Action = Action 40 | return cls._instance 41 | 42 | 43 | class DriverClient(Singleton): 44 | pass 45 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | """ 2 | @ Author:YueC 3 | @ Description: 4 | """ 5 | import pytest 6 | 7 | if __name__ == '__main__': 8 | pytest.main(['-v', '--maxfail=30', './tests/test_case/', '--alluredir', ' /report/test']) 9 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @ Author:YueC 4 | @ Description:Pytest hook Appium 5 | """ 6 | import datetime 7 | import os 8 | import sys 9 | import allure 10 | import pytest 11 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | from base.driver import DriverClient 13 | 14 | 15 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 16 | def pytest_runtest_makereport(item, call): 17 | # 用例报错捕捉 18 | Action = DriverClient().Action 19 | outcome = yield 20 | rep = outcome.get_result() 21 | if rep.when == "call" and rep.failed: 22 | f = Action.driver.get_screenshot_as_png() 23 | allure.attach(f, '失败截图', allure.attachment_type.PNG) 24 | logcat = Action.driver.get_log('logcat') 25 | c = '\n'.join([i['message'] for i in logcat]) 26 | allure.attach(c, 'APPlog', allure.attachment_type.TEXT) 27 | if Action.get_app_pid() != Action.apppid: 28 | raise Exception('设备进程 ID 变化,可能发生崩溃') 29 | 30 | 31 | def pytest_runtest_call(item): 32 | # 每条用例代码执行之前,非用例执行之前 33 | allure.dynamic.description('用例开始时间:{}'.format(datetime.datetime.now())) 34 | Action = DriverClient().Action 35 | if Action.get_app_pid() != Action.apppid: 36 | raise Exception('设备进程 ID 变化,可能发生崩溃') 37 | 38 | 39 | def pytest_collect_file(parent, path): 40 | # 获取文件.yml 文件 41 | if path.ext == ".yml" and path.basename.startswith("test"): 42 | return YamlFile(path, parent) 43 | 44 | 45 | class YamlFile(pytest.File): 46 | # 读取文件内容 47 | def collect(self): 48 | import yaml 49 | raw = yaml.safe_load(self.fspath.open(encoding='utf-8')) 50 | for name, values in raw.items(): 51 | yield YamlTest(name, self, values) 52 | 53 | 54 | @pytest.fixture 55 | def cmdopt(request): 56 | return request.config.getoption("--cmdopt") 57 | 58 | 59 | def yamldict(locator): 60 | """ 自动判断元素类型 61 | :param locator: 定位器 62 | :return: 63 | """ 64 | element = str(locator['element']) 65 | if not locator.get('type'): 66 | locator['type'] = 'xpath' 67 | if '//' in element: 68 | locator['type'] = 'xpath' 69 | elif ':id' in element: 70 | locator['type'] = 'id' 71 | elif 'android.' in element or 'XCUIElement' in element: 72 | locator['type'] = 'class name' 73 | else: 74 | locator['type'] = 'accessibility id' 75 | return locator 76 | 77 | 78 | class YamlTest(pytest.Item): 79 | def __init__(self, name, parent, values): 80 | super(YamlTest, self).__init__(name, parent) 81 | self.values = values 82 | self.Action = DriverClient().Action 83 | self.locator = None 84 | 85 | def runtest(self): 86 | # 运行用例 87 | for self.locator in self.values: 88 | self.locator['time'] = 5 89 | is_displayed = True 90 | if not self.locator.get('is_displayed'): 91 | is_displayed = False if str(self.locator.get('is_displayed')).lower() == 'false' else True 92 | try: 93 | if self.locator.get('element'): 94 | response = self.Action.__getattribute__(self.locator.get('method'))(yamldict(self.locator)) 95 | else: 96 | response = self.Action.__getattribute__(self.locator.get('method'))() 97 | self.assert_response(response, self.locator) 98 | except Exception as E: 99 | if is_displayed: 100 | raise E 101 | pass 102 | 103 | def repr_failure(self, excinfo): 104 | """自定义报错信息,如果没有定义则会默认打印错误堆栈信息,因为比较乱,所以这里自定义一下 """ 105 | if isinstance(excinfo.value, Exception): 106 | return '测试类名称:{} \n' \ 107 | '输入参数:{} \n' \ 108 | '错误信息:{}'.format(self.name, self.locator, excinfo.value.args) 109 | 110 | def assert_response(self, response, locator): 111 | if locator.get('assert_text'): 112 | assert locator['assert_text'] in response 113 | elif locator.get('assert_element'): 114 | assert response 115 | 116 | def reportinfo(self): 117 | return self.fspath, 0, "CaseName: %s" % self.name 118 | -------------------------------------------------------------------------------- /tests/test_case/test_search.yml: -------------------------------------------------------------------------------- 1 | test_search: 2 | - 3 | method: launchApp # 重启 APP 4 | - 5 | method: click 6 | element: click_ad_skip 7 | name: 广告跳过按钮 8 | is_displayed: False 9 | - 10 | method: click 11 | element: 发现 12 | name: 导航发现按钮 13 | - 14 | method: sleep 15 | element: 3 16 | - 17 | method: set_text 18 | element: com.sina.weibo:id/tv_search_keyword 19 | text: testerhome 20 | name: 搜索输入框 21 | - 22 | method: set_keycode_enter 23 | - 24 | method: screenshot_element 25 | element: //*[@resource-id="com.sina.weibo:id/lv_content"]/android.widget.RelativeLayout[1] 26 | name: 搜索内容截图 27 | - 28 | method: sleep 29 | element: 3 30 | 31 | -------------------------------------------------------------------------------- /tests/test_case/test_top.yml: -------------------------------------------------------------------------------- 1 | test_top: 2 | - 3 | method: launchApp # 重启 APP 4 | - 5 | method: click 6 | element: click_ad_skip 7 | name: 广告跳过按钮 8 | is_displayed: False 9 | - 10 | method: click 11 | element: 发现 12 | name: 导航发现按钮 13 | - 14 | method: swip_up 15 | - 16 | method: get_text 17 | element: com.sina.weibo:id/item_text 18 | name: 热门话题title 19 | assert_text: 热门话题 20 | - 21 | method: screenshot_element 22 | element: com.sina.weibo:id/stubCardArticleTitleLayout 23 | name: 第一条热门话题截图 -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from .log import Log as L 4 | -------------------------------------------------------------------------------- /utils/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | 5 | 6 | class Log: 7 | @staticmethod 8 | def e(msg, list_msg=[]): 9 | if list_msg: 10 | Log.show_list(msg, list_msg, Log.e) 11 | else: 12 | ColorLog.show_error(get_now_time() + " [Error]:" + "".join(msg)) 13 | 14 | @staticmethod 15 | def w(msg, list_msg=[]): 16 | if list_msg: 17 | Log.show_list(msg, list_msg, Log.w) 18 | else: 19 | ColorLog.show_warn(get_now_time() + " [Warn]:" + "".join(msg)) 20 | 21 | @staticmethod 22 | def i(msg, list_msg=[]): 23 | if list_msg: 24 | Log.show_list(msg, list_msg, Log.i) 25 | else: 26 | ColorLog.show_info(get_now_time() + " [Info]:" + "".join(msg)) 27 | 28 | @staticmethod 29 | def d(msg, list_msg=[]): 30 | if list_msg: 31 | Log.show_list(msg, list_msg, Log.d) 32 | else: 33 | ColorLog.show_debug(get_now_time() + " [Debug]:" + "".join(msg)) 34 | 35 | @staticmethod 36 | def show_list(msg, list_msg, f): 37 | temp = msg + "[ " + "\t".join(list_msg) + " ]" 38 | f(temp) 39 | 40 | 41 | class ColorLog: 42 | @staticmethod 43 | def c(msg, colour): 44 | try: 45 | from termcolor import colored, cprint 46 | p = lambda x: cprint(x, '%s' % colour) 47 | return p(msg) 48 | except: 49 | print(msg) 50 | 51 | @staticmethod 52 | def show_verbose(msg): 53 | ColorLog.c(msg, 'white') 54 | 55 | @staticmethod 56 | def show_debug(msg): 57 | ColorLog.c(msg, 'blue') 58 | 59 | @staticmethod 60 | def show_info(msg): 61 | ColorLog.c(msg, 'green') 62 | 63 | @staticmethod 64 | def show_warn(msg): 65 | ColorLog.c(msg, 'yellow') 66 | 67 | @staticmethod 68 | def show_error(msg): 69 | ColorLog.c(msg, 'red') 70 | 71 | 72 | def get_now_time(): 73 | return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) 74 | -------------------------------------------------------------------------------- /utils/shell.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import subprocess 3 | 4 | 5 | class Shell: 6 | @staticmethod 7 | def invoke(cmd): 8 | output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 9 | o = output.decode("utf-8") 10 | return o 11 | 12 | 13 | class Device: 14 | @staticmethod 15 | def get_android_devices(): 16 | android_devices_list = [] 17 | for device in Shell.invoke('adb devices').splitlines(): 18 | if 'device' in device and 'devices' not in device: 19 | device = device.split('\t')[0] 20 | android_devices_list.append(device) 21 | return android_devices_list 22 | 23 | 24 | class ADB(object): 25 | """ 26 | 参数: device_id 27 | """ 28 | 29 | def __init__(self, device_id=""): 30 | 31 | if device_id == "": 32 | self.device_id = "" 33 | else: 34 | self.device_id = "-s %s" % device_id 35 | 36 | def adb(self, args): 37 | cmd = "adb {0} {1}".format(self.device_id, str(args)) 38 | return Shell.invoke(cmd) 39 | 40 | def shell(self, args): 41 | 42 | cmd = "adb {0} shell {1}".format(self.device_id, str(args)) 43 | return Shell.invoke(cmd) 44 | 45 | def get_device_state(self): 46 | """ 47 | 获取设备状态: offline | bootloader | device 48 | """ 49 | return self.adb("get-state").strip() 50 | 51 | def connect_android_tcp(self, ip): 52 | """ 53 | 绑定设备信息 用于无线测试 54 | """ 55 | self.adb('tcpip 5555') 56 | return self.adb('connect {0}:5555'.format(ip)).strip() 57 | 58 | def disconnect_android_tcp(self, ip): 59 | """ 60 | 解除设备信息 用于无线测试 61 | """ 62 | self.adb('tcpip 5555') 63 | return self.adb('disconnect {0}:5555'.format(ip)).strip() 64 | 65 | def get_device_id(self): 66 | """ 67 | 获取设备id号,return serialNo 68 | """ 69 | return self.adb("get-serialno").strip() 70 | 71 | def get_android_version(self): 72 | """ 73 | 获取设备中的Android版本号,如4.2.2 74 | """ 75 | return self.shell( 76 | "getprop ro.build.version.release").strip() 77 | 78 | def get_sdk_version(self): 79 | """ 80 | 获取设备SDK版本号 81 | """ 82 | return self.shell("getprop ro.build.version.sdk").strip() 83 | 84 | def get_android_model(self): 85 | """ 86 | 获取设备型号 87 | """ 88 | 89 | return self.shell('getprop ro.product.model').strip() 90 | 91 | def get_android_ip(self): 92 | """ 93 | 获取设备IP 94 | """ 95 | return self.shell('netcfg | find "wlan0"').strip().split()[2].split('/')[0] 96 | 97 | def get_rcepageage_version(self): 98 | """ 99 | :return: (版本日期,版本号) 100 | """ 101 | return self.shell('dumpsys package cn.rongcloud.rce | findstr version').split() 102 | --------------------------------------------------------------------------------