【自动化】UI

一、参考资料

二、POM设计模式

2.1 POM的概念

  • POM全称为Page Object Model,这也是是目前最为经典的一种设计思想,用大白话说就是:将页面UI元素对象、逻辑、业务、数据等分离开来,使得代码逻辑更加清晰,复用性,可维护性更高的一种方法。
    pom.png

2.2 POM模式的优点

  • 让UI自动化更早介入项目中,可项目开发完再进行元素定位的适配与调试;

  • POM 将页面元素定位和业务操作流程分开,分离了测试对象和测试脚本;

  • 如果UI页面元素更改,测试脚本不需要更改,只需要更改页面对象中的某些代码就可以;

  • POM能让我们的测试代码变得可读性更好,高可维护性,高复用性;

  • 可多人共同维护开发脚本,利于团队协作。

三、准备Android自动化环境

3.1 安装adb(Android调试桥)

  1. 打开链接:http://adbdownload.com/,选择Windows版本;
  2. 下载完成后将压缩包解压到自定义目录;
  3. 设置环境变量:右键点击此电脑(我的电脑)-> 属性 -> 高级 -> 环境变量 -> 用户变量(或系统变量)-> 找到名为Path的变量 -> 点击编辑 -> 点击新建 -> 将adb的安装路径保存到变量中(例:E:\platform-tools)
  4. Win + R 打开CMD,输入adb version,确认是否安装成功。

3.2 安装allure

  1. 打开链接:https://github.com/allure-framework/allure2/releases
  2. 找到任意一个版本(推荐最新版本),Asserts底下,选择.zip格式,下载到本机自定义目录;
  3. 右键点击此电脑(我的电脑)-> 属性 -> 高级 -> 环境变量 -> 用户变量(或系统变量)-> 找到名为Path的变量 -> 点击编辑 -> 点击新建 -> 将 allure 解压之后的 allure 路径的 bin 目录路径放到环境变量当中。

3.3 移动设备(手机)设置

  1. 使用USB线连接手机与电脑两端;
  2. 手机上打开设置 -> 打开开发者选项 -> 打开允许USB调试;

四、执行Android测试

4.1 设置移动设备UUID

  • 打开控制台
  • 查看设备连接状态:adb devices,如果收到List of devices attached xxxxxxxxx device,代表手机成功连接电脑,复制下手机的udid;
  • 获取所有的包名:adb shell pm list package
  • 查看手机正在运行的APP名称的命令:adb logcat | findstr Displayed

4.2 定位Android端元素

  • 定位WebView元素方式:打开Google Chrome,在输入链接:chrome://inspect
  • 定位Android原生元素方式
    • 在pycharm控制台中输入命令
      • python -m uiautomator2 init
      • python -m weditor

五、代码

  • conftest.py
# 测试失败截图
img_path = os.path.join(os.getcwd(), 'report', 'fail_images')
app_album = os.path.join(os.getcwd(), 'report', 'app_album')
# 添加allure报告截图
# 文件不存在时,创建文件
if not os.path.exists(img_path):
    os.makedirs(img_path)
if not os.path.exists(app_album):
    os.makedirs(app_album)

logger = TestLogKit().logger
# 实例化配置文件读取对象
ini_read = IniRead()
ini_write = IniWrite()
_yml_read = YmlRead()
# 失败次数
failure_counts = 0
"""
1.在测试中,fixture为测试提供了一个定义好的、可靠的和一致的上下文。这可能包括环境(例如配置有已知参数的数据库)或内容(例如数据集);

2.由fixture设置的服务、状态或其他操作环境由测试函数通过参数访问。对于测试函数使用的每个fixture,
在测试函数的定义中通常都有一个参数(以fixture命名);

3.当pytest运行一个测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数具有相同名称的fixture。
一旦pytest找到它们,它就运行这些fixture,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。
"""

mobile = ini_read.read_section('Mobile')
app = ini_read.read_section('App')
lds_app_config = ini_read.read_section('LDSAPPConfig')
VarRepo.mobile = mobile


@pytest.fixture(scope=VarRepo.is_restart_app)
def init(launch_app):
    """
    移动设备(手机)初始化,用于连接、启动移动设备(手机)、连接WebView驱动。
    :return:
    """
    orig_driver = launch_app
    if Universal.android_os_validate():
        web_view_driver = WebViewDriver(VarRepo.orig_driver, mobile.port)
        web_driver = web_view_driver.connect()
        VarRepo.web_view_driver = web_view_driver
        # 首轮执行判断是否需要登录账号
        if VarRepo.web_driver is None:
            VarRepo.web_driver = web_driver
            aidot_app_login()
        current_url = Universal.get_current_url(web_driver)
    else:
        ios_orig_driver = VarRepo.ios_orig_driver
        ios_orig_driver.app_start()
        web_driver = VarRepo.ios_orig_driver
        current_url = VarRepo.ios_orig_driver.current_url
    VarRepo.app_index_url = current_url
    VarRepo.web_driver = web_driver
    yield orig_driver, web_driver


@pytest.fixture(scope=VarRepo.is_restart_app)
def launch_app(launch_orig):
    """
    使用原生驱动打开app 测试结束后关闭app
    :return:
    """
    orig_driver = launch_orig
    if Universal.android_os_validate():
        # 检查uiautomator2服务是否启动,否则重启uiautomator服务
        if not orig_driver.uiautomator.running():
            orig_driver.reset_uiautomator()
        android_orig_driver = VarRepo.android_orig_driver
        android_orig_driver.app_start(app, use_monkey=True)
    else:
        ios_orig_driver = VarRepo.ios_orig_driver
        ios_orig_driver.app_start()
    yield orig_driver
    if Universal.android_os_validate():
        # android_orig_driver.app_stop(app)
        pass
    else:
        ios_orig_driver = VarRepo.ios_orig_driver
        ios_orig_driver.app_stop()
        VarRepo.ios_orig_driver.release_port(mobile.server_port)

@pytest.fixture(scope="session")
def launch_orig():
    """
    加载UIAutomator2 系统原生操作驱动
    :return:
    """
    if mobile.os is None or mobile.os.lower() not in [OSType.ANDROID.value.lower(),
                                                      OSType.IOS.value.lower()]:
        raise ValueError
    VarRepo.app = app
    mqtt_auth = get_mqtt_connect_auth()
    if mqtt_auth and VarRepo.mq_client is None:
        mq_client = MQTTClient(VarRepo.lds_app_user_id,
                               mqtt_auth.get('host'),
                               mqtt_auth.get('password'),
                               mqtt_auth.get('heartbeat'),
                               mqtt_auth.get('clientId'))
        mq_client.run_mq_client()
        VarRepo.mq_client = mq_client
    get_robotic_arm()
    if Universal.android_os_validate():
        ADBKit().light_up_screen()
        VarRepo.mobile = mobile
        # 实例化Android原生操作驱动类的对象
        android_orig_driver = AndroidOrigDriver()
        orig_driver = android_orig_driver.connect(mobile)
        VarRepo.android_orig_driver = android_orig_driver
    else:
        ios_orig_driver = IOSOrigDriver()
        orig_driver = ios_orig_driver.connect(mobile, app)
        VarRepo.ios_orig_driver = ios_orig_driver
    VarRepo.orig_driver = orig_driver
    yield orig_driver
    LdsAPIReq.logout()

def get_mobile_info():
    cmd = 'adb devices -l'
    try:
        mobile_phone_id = os.popen(cmd).readlines()[1].split()[0]
        ini_write.write_option("Mobile", "mobile_phone_id", mobile_phone_id)
        mobile_phone_model = os.popen(cmd).readlines()[1].split()[3].split(":")[1]
        ini_write.write_option("Mobile", "model", mobile_phone_model)
    except Exception as e:
        logger.error("请检查手机是否连接:{}".format(e.__str__()))


def is_restart_app():
    """
    保存是否重启APP的变量到变量仓库
    :return:
    """
    _case_data = _yml_read.get_data('case')
    VarRepo.case_data = _case_data
    if _case_data.is_restart_app == 1:
        VarRepo.is_restart_app = "function"
    else:
        VarRepo.is_restart_app = "session"

5.1 通用的方法

  • /base/orig_base_operation.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-

from base.base_operation.android_orig_base_operation import AndroidOrigBaseOperation
from base.base_operation.ios_base_operation import IOSBaseOperation
from base.common.universal.universal import Universal
from base.model.element import Element


class OrigBaseOperation(AndroidOrigBaseOperation, IOSBaseOperation):
    def __init__(self, orig_driver):
        """
        包括Android、iOS原生元素的基本操作的类
        :param orig_driver: 可传入Android或iOS原生驱动(Appium、UIAutomator2)
        """
        AndroidOrigBaseOperation.__init__(self, orig_driver)
        IOSBaseOperation.__init__(self, orig_driver)

    def find_xpath_by_orig(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True,
                           **kwargs) -> any:
        """
        【查找】原生元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮'}
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回None。
        :param throws_err: 是否抛出异常。
        :return:
        """
        if Universal.android_os_validate():
            result = self.find_xpath_by_android_orig(element, wait_seconds, throws_err, **kwargs)
            return result
        return self.find_by_ios(element, wait_seconds, throws_err, **kwargs)

    def find_xpath_list_by_orig(self, element: Element, throws_err: bool = True) -> list:
        """
        【查找】原生元素,存在返回找到的元素列表,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮'}
        :param throws_err: 是否抛出异常。
        :return:
        """
        if Universal.android_os_validate():
            return self.find_xpath_list_by_android_orig(element, throws_err)
        return self.find_by_ios(element, return_list=True)

    def find_by_orig(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True, **kwargs) -> any:
        """
        【查找】原生元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮', 'by': 'id'}
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :return:
        """
        if Universal.android_os_validate():
            return self.find_by_android_orig(element, wait_seconds=wait_seconds, throws_err=throws_err, **kwargs)
        return self.find_by_ios(element, wait_seconds, throws_err, **kwargs)

    def click_by_orig(self, element: Element, throws_err: bool = True) -> bool:
        """
        行【点击】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        if Universal.android_os_validate():
            return self.click_by_android_orig(element, throws_err)
        return self.click_by_ios(element, throws_err)

    def input_by_orig(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过WebView方式执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮', 'text': '输入内容'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        if Universal.android_os_validate():
            return self.input_by_android_orig(element, throws_err)
        return self.input_by_ios(element, throws_err)

  • /base/web_view_base_operation.py
from base.base_operation.android_web_base_operation import AndroidWebBaseOperation
from base.base_operation.ios_base_operation import IOSBaseOperation
from base.common.universal.universal import Universal
from base.model.element import Element


class WebViewBaseOperation(AndroidWebBaseOperation, IOSBaseOperation):
    def __init__(self, web_view_driver):
        """
        包括Android、iOS WebView元素的基本操作的类
        :param web_view_driver: 可传入Android或iOS原生驱动(Appium、Selenium)
        """
        AndroidWebBaseOperation.__init__(self, web_view_driver)
        IOSBaseOperation.__init__(self, web_view_driver)

    def is_exists_by_web_view(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True,
                              **kwargs):
        if Universal.android_os_validate():
            return self.is_exists_by_android_web(element, wait_seconds, throws_err, **kwargs)
        return self.is_exists_by_ios(element, wait_seconds, throws_err, **kwargs)

    def find_by_web_view(self, element: Element, wait_seconds: (int, float) = 2, return_list: bool = False,
                         throws_err: bool = True, **kwargs) -> any:
        """
        【查找】WebView元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'by': By.XPATH, 'element': xxx, 'element_name': '登录按钮'}。
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param return_list: 如果为True,则返回找到的元素的列表(1个或多个),反之返回单个元素。
        :param throws_err: 是否抛出异常。
        :return: any
        """
        if Universal.android_os_validate():
            return self.find_by_android_web(element, wait_seconds, return_list, throws_err, **kwargs)
        return self.find_by_ios(element, wait_seconds, throws_err, return_list, **kwargs)

    def click_by_web_view(self, element: Element, throws_err: bool = True, **kwargs) -> bool:
        """
        通过WebView方式执行【点击】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        if Universal.android_os_validate():
            return self.click_by_android_web(element, throws_err, **kwargs)
        return self.click_by_ios(element, throws_err, **kwargs)

    def input_by_web_view(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过WebView方式执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮', 'text': '输入内容'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        if Universal.android_os_validate():
            return self.input_by_android_web(element, throws_err)
        return self.input_by_ios(element, throws_err)

    def get_text_by_web_view(self, element: Element, throws_err: bool = True, **kwargs) -> (str, int, float):
        """
        获取APP UI上的文本
        :param element: 元素,{'element': xxx, 'element_name': '登录输入框', 'get_method': 'text'}。
        :param throws_err: 是否抛出异常。
        :return:
        """
        if Universal.android_os_validate():
            return self.get_text_by_android_web(element, throws_err, **kwargs)
        return self.input_by_ios(element, throws_err, **kwargs)

    def back_by_web(self):
        """
        原生返回
        """
        if Universal.android_os_validate():
            self.back_by_android_web()
        else:
            self.back_by_ios()

    def to_app_index_by_web(self):
        if Universal.android_os_validate():
            self.to_app_index_by_android_web()
        else:
            self.to_app_index_by_ios()

5.2 /base/driver

  • /base/driver/android_orig_driver.py
import uiautomator2 as u2

from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import Universal
from base.common.universal.var_repo import VarRepo
from base.model.struct import Struct


class AndroidOrigDriver:
    def __init__(self):
        """
        UIAutomator2的驱动工具类,用于操作Android原生元素。
        """
        # 传入手机ID(序列号),使用UIAutomator2连接到手机(设备)
        self._driver = None
        self._logger = TestLogKit().logger

    def connect(self, mobile: Struct):
        self._logger.info(f'当前移动设备(手机)信息:{mobile}')
        self._driver = u2.connect(mobile.mobile_phone_id)
        VarRepo.mobile = mobile
        return self._driver

    def app_start(self, app: Struct, **kwargs):
        """
        启动APP
        :param app: APP信息。
        :return:
        """
        if app.gms_package:
            self._driver.app_stop(app.gms_package)
        self._driver.app_start(app.app_package, stop=True, **kwargs)
        self._logger.info(f'已启动APP:{app.app_package}。')
        Universal.wait(2)

    def app_stop(self, app: Struct):
        """
        停止APP
        :param app: APP信息。
        :return:
        """
        if app.gms_package:
            self._driver.app_stop(app.gms_package)
        # 传入APP包名,使用【连接驱动】启动APP
        self._driver.app_stop(app.app_package)
        self._logger.info(f'已停止APP:{app.app_package}。')
        Universal.wait(2)

    def wake_and_swipe_up(self):
        """
        唤醒屏幕并【向上】滑动屏幕解锁
        :return:
        """
        self._driver.screen_on()
        Universal.wait(2)
        self._driver.swipe(0.5, 0.1, 0.5, 0.5, 0.1)
        self._logger.info(f'已唤醒移动设备(手机)并向上滑动解锁屏幕。')

    def swipe(self, x1: (int, float), y1: (int, float), x2: (int, float), y2: (int, float), duration=None):
        """
        滑动屏幕
        :return:
        """
        self._driver.swipe(x1, y1, x2, y2, duration)

    def long_click(self, rect: tuple, desc: str, duration: float):
        """
        长按目标坐标
        @rect:坐标
        @duration:长按时间
        @desc: 描述
        :return:
        """
        self._logger.info(f'长按{desc}')
        self._driver.long_click(rect[0], rect[1], duration)

  • /base/driver/ios_orig_driver.py
import os
import platform
import socket
import subprocess
from time import ctime

from appium import webdriver
from appium.webdriver.webdriver import WebDriver

from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import Universal
from base.model.struct import Struct


class IOSOrigDriver:
    def __init__(self):
        """
        使用[Appium]构造针对[iOS]设备的元素操作驱动。
        """
        # 传入手机ID(序列号),使用Appium连接到手机(设备)
        self._appium_driver = None
        self._logger = TestLogKit().logger

    def connect(self, mobile: Struct, app: Struct) -> WebDriver:
        """
        连接iOS设备的操作驱动
        :param mobile:
        :param app:
        :return:
        """
        self._logger.info(f'当前移动设备(手机)信息:{mobile}')
        self._appium_server_start(mobile, app)
        desired_caps = {
            "udid": mobile.mobile_phone_id,
            "deviceName": mobile.mobile_phone_id,
            "platformName": mobile.os,
            "bundleId": app.app_package,
            "webDriverAgentUrl": app.web_driver_agent_url,
            "automationName": "XCUITest",
            "usePrebuiltWDA": False,
            "useNewWDA": False,
            "useXctestrunFile": False,
            "noReset": True,
            "xcodeOrgId": "23X82Z4W7K",
            "xcodeSigningId": "iPhone Developer",
            "startIWDP": True,
            "systemPort": mobile.system_port,
            'newCommandTimeout': 3600
        }
        self._appium_driver = webdriver.Remote(f'http://{str(app.ip)}:{mobile.server_port}/wd/hub', desired_caps)
        self._logger.info(f'已连接到iOS设备。')
        Universal.wait(5)
        return self._appium_driver

    def app_start(self):
        self._appium_driver.launch_app()
        self._logger.info(f'已启动APP。')
        Universal.wait(5)

    def app_stop(self):
        self._appium_driver.quit()
        self._logger.info(f'已停止APP。')
        Universal.wait(2)

    def switch_context(self, switch_type: int):
        """
        切换原生与H5
        :param switch_type: 传1:切换到APP原生,传2:切换到APPWebView。
        :return:
        """
        # 获取所有的容器
        contexts = self._appium_driver.contexts
        if len(contexts) > 1:
            current_context = self._appium_driver.current_context
            if switch_type == 1 and current_context != contexts[0]:
                self._appium_driver.switch_to.context(contexts[0])
                self._logger.info('切换到iOS APP 原生。')
            elif switch_type == 2 and current_context != contexts[1]:
                self._appium_driver.switch_to.context(contexts[1])
                self._logger.info('切换到iOS APP WebView。')
        self._logger.debug(f'APP的Contexts: {contexts},当前上下文:{self._appium_driver.current_context}。')

    def _appium_server_start(self, mobile: Struct, app: Struct):
        # MAC端的Appium Server启动命令
        cmd = (f'node /opt/homebrew/lib/node_modules/appium/build/lib/main.js '
               f'-a {app.ip} '
               f'-p {mobile.server_port} '
               f'-bp {str(int(mobile.server_port) + 1)} '
               f'-U {mobile.mobile_phone_id} '
               f'--no-reset --session-override --log-timestamp --local-timezone --log-level error:warn')

        self._logger.info(f'{cmd} at {ctime()}')

        try:
            subprocess.Popen(cmd, shell=True)
            self._logger.info('正在启动Appium。')
            Universal.wait(13)
        except Exception as e:
            self._logger.error(f'启动Appium时出现意外问题(可根据实际情况忽略):{e.__str__()}')
            return True

    # 检测指定的端口是否被占用
    def _release_node(self):
        platform_name = platform.system()
        cmd_kill = 'killall -9 node'

        if platform_name == 'Windows':
            cmd_kill = 'taskkill /F /IM node.exe'
        os.popen(cmd_kill)
        self._logger.info("释放所有Node端口。")

    def check_port(self, mobile: Struct):
        """
        检测指定的端口是否被占用
        :return:
        """
        # 创建socket对象
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            s.connect((mobile.ip, int(mobile.port)))
            s.shutdown(2)
            self._logger.info(f'端口[{mobile.server_port}]未被占用。')
            return True
        except Exception as e:
            self._logger.info(f'端口[{mobile.server_port}]已被占用,原因:{e.__str__()}。')
            return False

    def release_port(self, port):
        """
        释放指定的端口
        :param port:
        :return:
        """
        platform_name = platform.system()
        # 查找对应端口的pid
        if platform_name == 'Windows':
            # 获取端口详情
            result = os.popen(f'netstat -ano | findstr {port}').read()

            if str(port) and 'LISTENING' in result:
                # 获取端口对应的pid进程
                i = result.index('LISTENING')
                start = i + len('LISTENING') + 7
                end = result.index('\n')
                pid = result[start:end]

                # 关闭被占用端口的pid
                cmd_kill = 'taskkill /F /pid {}'.format(pid)
                os.popen(cmd_kill)

        else:
            # 获取端口详情
            result = os.popen(f'lsof -i:{port}').readlines()[-1]
            pid = 0

            if str(port) and 'LISTEN' in result:

                for i in result.split(' '):
                    if i.isdigit():
                        pid = int(i)

                # 关闭被占用端口的pid
                cmd_kill = 'kill -9 {}'.format(pid)
                os.popen(cmd_kill)

                self._logger.info(f'已释放[{port}]端口。')
  • /base/driver/web_view_driver.py
import atexit
import subprocess

import uiautomator2 as u2
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver

from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import Universal
from config.static_read import StaticRead


class WebViewDriver:
    def __init__(self, u2_driver: u2.Device, port: int):
        """
        WebView的驱动工具类
        :param u2_driver: Android原生操作驱动(UIAutomator2)
        :param port: 移动设备(手机)端口
        """
        self._u2_driver = u2_driver
        self._port = port
        self._logger = TestLogKit().logger
        Universal.kill_port(self._port)

    def connect(self) -> WebDriver:
        """
        连接到APP,初始化WebView驱动,返回一个WebView驱动对象
        :return: WebDriver
        """
        # 获取当前APP
        current_app = self._u2_driver.app_current()
        current_get = current_app.get
        url = f'http://localhost:{self._port}'
        self._logger.info(f'开始连接WebView的操作驱动···')
        self._logger.info(f'连接信息:'
                          f'端口:[{self._port}] | '
                          f'设备序列号:[{self._u2_driver.serial}] | '
                          f'APP:[{current_get("package")}] | '
                          f'Activity:[{current_get("activity")}] | '
                          f'Web Driver URL: [{url}]')
        capabilities = {
            'chromeOptions': {
                'androidDeviceSerial': self._u2_driver.serial,
                'androidPackage': current_get('package'),
                'androidUseRunningApp': True,
                'androidProcess': current_get('package'),
                'androidActivity': current_get("activity"),
                'w3c': False}
        }

        self._launch_web_driver()
        driver = webdriver.Remote(url, desired_capabilities=capabilities)
        atexit.register(driver.quit)
        self._logger.info(f'已连接到WebView的操作驱动。')
        Universal.wait(2)
        return driver

    def _launch_web_driver(self):
        """
        启动WebDriver
        :return:
        """
        p = subprocess.Popen([StaticRead.get_chrome_driver_path(), '--port=' + str(self._port)])
        try:
            p.wait(timeout=2.0)
            return False
        except subprocess.TimeoutExpired:
            return True

5.3 base/base_operation

  • android_orig_base_operation.py
import os
from typing import Union

import ulid
from uiautomator2 import UiObject
from uiautomator2.xpath import XPathSelector, XMLElement

from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import Universal, loop
from base.model.element import Element


class AndroidOrigBaseOperation:
    """
    将UI自动化的一些通用操作抽象出来,例如点击、滑动及判断元素是否存在等。
    """

    def __init__(self, orig_driver):
        # Android 原生操作驱动
        self.orig_driver = orig_driver
        # 日志记录器
        self.logger = TestLogKit().logger

    @loop()
    def find_xpath_by_android_orig(self, element: Element, wait_seconds: (int, float) = 2,
                                   throws_err: bool = True) -> any:
        """
        【查找】Android原生元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮'}
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回None。
        :param throws_err: 是否抛出异常。
        :return:
        """
        try:
            element.obj = self.orig_driver.xpath(element.path).wait(wait_seconds)
            if element.obj is not None:
                self.logger.info(f'找到Android原生元素[{element.name}]。')
            return element.obj
        except Exception as e:
            Universal.return_fail(throws_err, e,
                                  log_text=f'查找Android原生元素[{element.to_str()}]'
                                           f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    @loop()
    def find_xpath_list_by_android_orig(self, element: Element, throws_err: bool = True) -> any:
        """
        【查找】Android原生元素,存在返回找到的元素列表,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮'}
        :param throws_err: 是否抛出异常。
        :return:
        """
        try:
            element.obj = self.orig_driver.xpath(element.path).all()
            self.logger.info(f'找到Android原生元素[{element.name}]。')
            return element.obj
        except Exception as e:
            Universal.return_fail(throws_err, e,
                                  log_text=f'查找Android原生元素[{element.to_str()}]'
                                           f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    @loop()
    def find_by_android_orig(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True) -> any:
        """
        【查找】Android原生元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮', 'by': 'id'}
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :return:
        """
        by = element.by
        if by not in ['classname', 'id', 'text', 'xpath']:
            Universal.return_fail(throws_err, ValueError('Key[by]的值应为classname、id、text或xpath其中一项。'), None)
        try:
            flag = False
            found = self._find_by(element)
            if by == 'xpath':
                found = found.wait(timeout=wait_seconds)
                if found:
                    flag = True
            elif found.exists(timeout=wait_seconds):
                flag = True
            else:
                flag = False
            if flag:
                self.logger.info(f'找到Android原生元素[{element.name}]。')
                element.obj = found
                return element.obj
            else:
                raise ValueError(f'未找到Android原生元素[{element.name}]。')
        except Exception as e:
            Universal.return_fail(throws_err, e,
                                  log_text=f'查找Android原生元素[{element.to_str()}]'
                                           f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')
        return None

    @loop()
    def click_by_android_orig(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过原生方式执行【点击】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            element.obj.click()
            self.logger.info(f'成功点击Android原生元素[{element.name}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False,
                                  f'点击Android原生元素[{element.to_str()}]出现意外问题(可根据实际情况忽略):{e.__str__()}')

    @loop()
    def input_by_android_orig(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过原生方式执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,Element对象
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        text = element.input_text
        try:
            found = self._find_by(element)
            found.set_text(text)
            self.logger.info(f'查找到并[{element.name}]成功输入[{text}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False,
                                  f'查找并输入内容时({element.to_str()})出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    @loop()
    def find_and_click_by_orig(self, element: Element, throws_err: bool = True) -> bool:
        """
        查找并点击元素
        :param element: 元素,Element对象
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            found = self._find_by(element)
            found.click()
            self.logger.info(f'找到Android原生元素[{element.name}]并成功点击。')
            element.obj = found
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e,
                                  log_text=f'查找并点击Android原生元素[{element.to_str()}]'
                                           f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    def input_by_adb(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过ADB与原生方式执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,Element对象
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        text = element.input_text
        try:
            element.obj.click()
            self.orig_driver.clear_text()
            os.system(f'adb shell input text {text}')
            self.logger.info(f'通过adb shell的方式[{element.name}]成功输入[{text}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'通过adb shell的方式输入内容时({element.to_str()})'
                                  f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    def _find_by(self, element: Element):
        """
        根据by参数来决定用什么方式查找元素,并返回查找结果
        :param element: 元素,Element对象
        :return:
        """
        path = element.path
        by = element.by
        if by == 'classname':
            found = self.orig_driver(className=path)
        elif by == 'id':
            found = self.orig_driver(resourceId=path)
        elif by == 'text':
            found = self.orig_driver(text=path)
        else:
            found = self.orig_driver.xpath(path)
        return found

    def click_location_by_orig(self, x, y, throws_err: bool = True) -> bool:
        """
        行【点击】动作,点击成功返回True,否则返回False。
        :param x: x坐标
        :param y: y坐标
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            self.orig_driver.click(x, y)
            self.logger.info(f"成功点击坐标({x},{y})")
            return True
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'{e.__str__()}。')

    def find_similar_image(self, img_path: str, throws_err: bool = True):
        try:
            point = None
            re = self.orig_driver.image.match(img_path)
            self.logger.info(f"成功匹配{img_path}")
            if re is not None:
                point = re.get("point")
            return point
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'{e.__str__()}。')

    def find_and_click_similar_image(self, img_path: str, timeout: float = 20.0, throws_err: bool = True):
        try:
            self.orig_driver.image.click(img_path, timeout=timeout)
            self.logger.info(f"成功匹配{img_path},并点击")
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'{e.__str__()}。')

    def scroll_to_text_by_android_orig(self, text, direction: str = "vert", throws_err=True):
        try:
            if direction == "horiz":
                self.orig_driver(scrollable=True).horiz.to(text=text)
            else:
                self.orig_driver(scrollable=True).to(text=text)
            self.logger.info(f"成功滚动到{text}")
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'{e.__str__()}。')

    def watch_context_and_click(self, xpath: str, throws_err=True):
        try:
            with self.orig_driver.watch_context() as ctx:
                ctx.when(xpath).click()
                self.logger.info(f"监听弹窗并点击{xpath}")
                ctx.stop()
        except Exception as e:
            Universal.return_fail(throws_err,
                                  e,
                                  False,
                                  f'{e.__str__()}。')

    def back_to_previous_page(self, wait_time: int = 1):
        """
        点击APP页面的左上角的左箭头返回上一页
        :param wait_time: 等待时间
        :return:
        """
        self.click_location_by_orig(0.072, 0.06, throws_err=False)
        Universal.wait(wait_time, '返回上一页')

    @staticmethod
    def screenshot_em_by_orig(element: Union[UiObject, XPathSelector, XMLElement], target_dir: str = os.getcwd()):
        """
        将元素截图并保存在指定路径
        :param element: Element对象
        :param target_dir: 将图片保存到目标路径,只详细到目录。
        :return:
        """
        if (not isinstance(element, UiObject)
                and not isinstance(element, XPathSelector)
                and not isinstance(element, XMLElement)):
            raise TypeError(f'{element}不是UiObject、XPathSelector与XMLElement对象。')
        if not os.path.exists(target_dir):
            raise NotADirectoryError(f'{target_dir}不存在。')
        image_ = element.screenshot()
        target_path = os.path.join(target_dir, f'{ulid.new().str}.jpg')
        image_.save(target_path)
        Universal.wait(1, f'成功保存元素截图到:[{target_path}]')
        return target_path

    @loop()
    def long_click_by_android_orig(self, element: Element, throws_err: bool = True) -> bool:
        try:
            element.obj.long_click()
            self.logger.info(f'成功长按Android原生元素[{element.name}]。')
            return True
        except Exception as e:
            Universal.return_fail(throws_err, e,
                                  log_text=f'长按Android原生元素[{element.to_str()}]'
                                           f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

  • android_web_base_operation.py
from typing import Union

from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import TouchActions, ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from uiautomator2 import UiObject
from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import loop, Universal
from base.common.universal.var_repo import VarRepo
from base.model.element import Element


class AndroidWebBaseOperation:
    """
    将UI自动化的一些通用操作抽象出来,例如点击、滑动及判断元素是否存在等。(Web View)
    """

    def __init__(self, web_view_driver: WebDriver):
        # Android WebView操作驱动
        self.web_view_driver = web_view_driver
        # 日志记录器
        self.logger = TestLogKit().logger

    @loop()
    def is_exists_by_android_web(self, element: Element, wait_seconds: (int, float) = 2,
                                 throws_err: bool = True) -> bool:
        """
        判断WebView元素【是否存在】,如果存在返回True,否则返回False。
        :param element: 元素,{'by': By.XPATH, 'element': xxx, 'element_name': '登录按钮'}。
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            WebDriverWait(self.web_view_driver,
                          wait_seconds).until(ec.presence_of_element_located((element.by,
                                                                              element.path)),
                                              message='not find')
            self.logger.info(f'存在WebView元素[{element.name}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False,
                                  f'判断WebView元素是否存在时({element.name})出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    @loop()
    def find_by_android_web(self, element: Element, wait_seconds: (int, float) = 2, return_list: bool = False,
                            throws_err: bool = True) -> any:
        """
        【查找】WebView元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'by': By.XPATH, 'element': xxx, 'element_name': '登录按钮'}。
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param return_list: 如果为True,则返回找到的元素的列表(1个或多个),反之返回单个元素。
        :param throws_err: 是否抛出异常。
        :return: any
        """
        if element.by == "text":
            element.path = f"//*[contains(text(),'{element.path}')]"
            element.by = By.XPATH
        method = ec.presence_of_all_elements_located if return_list else ec.presence_of_element_located
        try:
            element.obj = WebDriverWait(self.web_view_driver,
                                        wait_seconds).until(method((element.by, element.path)),
                                                            message=f'找不到{element.name}。')
            self.logger.info(f'查找到WebView元素[{element.name}]。')
            element.result = True
            return element.obj
        except Exception as e:
            msg = f'未查找到元素[{element.to_str()}]:{e.__str__()}'
            if not throws_err:
                msg = f'{msg},继续执行。'
            Universal.return_fail(throws_err, e, log_text=msg)

    @loop()
    def click_by_android_web(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过WebView方式执行【点击】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            element.obj.click()
            self.logger.info(f'成功点击WebView元素[{element.name}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'点击WebView元素失败[{element.name}]:{e.__str__()}')

    @loop()
    def tap_by_android_web(self, element: Element, throws_err: bool = True) -> bool:
        """
        当click失效的时候可以采用这种方式,而且这边的Action必须是selenium的
        只需要传递element即可。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            self.logger.debug(f'开始实例化TouchActions。')
            action = TouchActions(self.web_view_driver)
            self.logger.debug(f'完成实例化TouchActions。')
            action.tap(element.obj).perform()
            self.logger.info("成功点击[{}]".format(element.name))
            element.result = True
            return element.result
        except Exception as e:
            del action
            Universal.return_fail(throws_err, e, False, f'点击失败[{element.to_str()}]:{e.__str__()}')

    @loop()
    def input_by_android_web(self, element: Element, throws_err: bool = True) -> bool:
        """
        通过WebView方式执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮', 'text': '输入内容'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            text = element.input_text
            obj = element.obj
            element_name = element.name
            obj.send_keys(Keys.CONTROL, 'a')
            obj.send_keys(Keys.DELETE)
            obj.send_keys(text)
            obj.send_keys(Keys.RETURN)
            self.logger.info(f'[{element_name}]成功输入[{text}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'输入文本失败[{element.input_text}]:{e.__str__()}')

    @loop()
    def get_text_by_android_web(self, element: Element, throws_err: bool = True) -> (str, int, float):
        """
        获取APP UI上的文本
        :param element: 元素,{'element': xxx, 'element_name': '登录输入框', 'get_method': 'text'}。
        :param throws_err: 是否抛出异常。
        :return:
        """
        try:
            method = element.method
            if method not in ['text', 'attr']:
                raise ValueError(f'获取文本错误:{element}')
            em_obj = element.obj
            element.result = em_obj.text if method == 'text' else em_obj.attrib
            self.logger.info(f'通过[{method}]方式获取到文本[{element.result}]。')
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, log_text=f'获取文本失败[{element.name}]:{e.__str__()}')

    @loop()
    def find_sub_element_by_android_web(self, em: WebElement, sub_em: Element) -> (Element, None):
        """
        查找元素下的子元素。
        :param em: 元素。
        :param sub_em: {'element': xxx, 'element_name': '登录输入框'}
        :return:
        """
        locator = f'//div[@id="{em.get_attribute("id")}"]{sub_em.path}'
        try:
            sub_em.obj = em.find_element(By.XPATH, locator)
            self.logger.info(f'查找到[{sub_em.name}]。')
            return sub_em
        except NoSuchElementException:
            self.logger.info(f'未查找到子元素[{locator}]。')
        except Exception as e:
            self.logger.info(f'查找子元素[{locator}]时出现意外问题(可根据实际情况忽略):{e.__str__()}。')
            raise e
        return None

    @loop()
    def slider_offset(self, element: Element, position, throws_err: bool = True) -> bool:
        """
        滑动滑块,以滑块的中心点为标准进行移动
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param position: 滑动的偏移值,x:向右移动为负值,向上移动为负值,
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            action = TouchActions(self.web_view_driver)
            action.scroll_from_element(element.path, position.get('x', 0), position.get('y', 0)).perform()
            self.logger.info(f'成功滚动元素[{element.name}],偏移值为:{position}')
            element.result = True
            return element.result
        except Exception as e:
            del action
            Universal.return_fail(throws_err, e, False, f'点击失败[{element.name}]:{e.__str__()}')

    @loop()
    def slider_content(self, position, throws_err: bool = True) -> (str, int, float):
        """
        滚动页面 指定偏移值
        :param position: 滑动的偏移值,x:向右移动为负值,向上移动为负值,
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            action = TouchActions(self.web_view_driver)
            action.scroll(position.get('x', 0), position.get('y', 0)).perform()
            self.logger.info(f'成功滚动元素,偏移值为:{position}')
            return True
        except Exception as e:
            del action
            Universal.return_fail(throws_err, e, log_text=f'滚动失败:{e.__str__()}')

    def scroll_into_view(self, element, throws_err: bool = True):
        """将元素滚动到可见区域,与页面的顶部对齐"""
        try:
            self.web_view_driver.execute_script("arguments[0].scrollIntoView(false);", element)
            self.logger.info("成功将元素滚动到可视区域")
            element.result = True
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'滚动失败:{e.__str__()}')

    def find_and_click(self, element: Element, wait_seconds: (int, float) = 2,
                       throws_err: bool = True, interval: int = 1, retry: int = 1):
        """
        【查找并点击】WebView元素,存在返回True,否则返回None。
        :param element: 元素,{'by': By.XPATH, 'element': xxx, 'element_name': '登录按钮'}。
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :param interval: 找到后间隔多久点击
        :param retry: 重试次数
        :return: any
        """
        re = self.find_by_android_web(element, wait_seconds=wait_seconds, throws_err=throws_err, retry=retry)
        Universal.wait(interval)
        if re is not None:
            self.click_by_android_web(element, throws_err=throws_err, retry=retry)
            element.result = True
        return element.result

    def long_press(self, element: UiObject, element_name: str, throws_err: bool = True):
        """
        长按某个元素
        :param element: 通过原生方式查找的元素
        :param element_name: 元素名称
        :param throws_err: 是否抛出错误
        :return: None
        """
        try:
            element.long_click()
            self.logger.info(f"成功长按[{element_name}]")
            element.result = True
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'长按元素失败[{element_name}]:{e.__str__()}')

    def click_by_js(self, element):
        self.web_view_driver.execute_script("arguments[0].click();", element)

    def back_by_android_web(self, throws_err=True):
        try:
            self.web_view_driver.back()
            self.logger.info("成功返回上级页面")
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'通过webview返回失败:{e.__str__()}')

    def to_app_index_by_android_web(self, throws_err=True):
        try:
            self.web_view_driver.get(VarRepo.app_index_url)
            self.logger.info("成功返回首页")
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'通过webview返回首页失败:{e.__str__()}')

    def slider_sliding(self, em: Element, x_offset: Union[int, float], y_offset: Union[int, float],
                       throws_err: bool = True):
        """
        往上/下/左/右方向滑动滑块
        :param em: obj不为空的Element
        :param x_offset: x轴目标位置
        :param y_offset: y轴目标位置
        :param throws_err: 是否抛出错误
        :return: None
        """
        try:
            action = ActionChains(self.web_view_driver)
            action.drag_and_drop_by_offset(em.obj, x_offset, y_offset).perform()
            self.logger.info(f'向({x_offset},{y_offset})滑动成功')
        except Exception as e:
            del action
            Universal.return_fail(throws_err, e, False, f'通过WebView滑动滑块时出现意外错误:{e.__str__()}')

    def scroll_time_picker(self, em: WebElement, steps: int, up: bool = True):
        """
        竖向滚动时间选择器
        :param em: 通过Selenium查找到的时间选择器的元素
        :param steps: 滚动步数,10的倍数
        :param up: 是否向上滚动
        :return:
        """
        try:
            base_digital = 10
            self.logger.info(f'即将以[{base_digital}]像素的范围滚动[{steps}]Steps')
            touch_actions = TouchActions(self.web_view_driver)
            counts = steps * base_digital
            while counts > 0:
                touch_actions = touch_actions.scroll_from_element(em, 0, base_digital if up else -base_digital)
                counts -= base_digital
            touch_actions.perform()
            self.logger.info(f'成功滚动[{steps}]Steps')
        except Exception as e:
            del touch_actions
            self.logger.warning(f'滚动时出现意外错误:{e.__str__()}')

  • ios_base_operation.py
import time

from selenium.webdriver.common.keys import Keys

from base.common.log.test_log_kit import TestLogKit
from base.common.universal.universal import Universal, loop, context_listening
from base.common.universal.var_repo import VarRepo
from base.model.element import Element


class IOSBaseOperation:
    """
    1.将UI自动化的一些通用操作抽象出来,例如点击、滑动及判断元素是否存在等。
    2.由于iOS APP自动化使用的是Appium框架,而Appium驱动均可通过原生及WebView两种方式操作元素,
    所以统一将原生及WebView的通用操作放到该文件中。
    """

    def __init__(self, ios_driver):
        self.ios_driver = ios_driver
        self.logger = TestLogKit().logger

    @context_listening
    def is_exists_by_ios(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True) -> bool:
        """
        判断iOS原生/WebView元素【是否存在】,如果存在返回True,否则返回False。
        :param element: 元素,{'by': By.XPATH, 'element': xxx, 'element_name': '登录按钮'}。
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        start = time.time()
        while time.time() - start <= wait_seconds:
            result = self.ios_driver.find_elements(element.by, element.path)
            if result:
                self.logger.info(f'找到iOS元素[{element.name}]。')
                element.result = True
                return element.result
            time.sleep(0.1)
        Universal.return_fail(throws_err, ValueError(), False, f'未查找到iOS元素[{element.to_str()}]')

    @context_listening
    @loop()
    def find_by_ios(self, element: Element, wait_seconds: (int, float) = 2, throws_err: bool = True,
                    return_list: bool = False) -> any:
        """
        【查找】iOS原生/WebView元素,存在返回找到的元素,否则返回None。
        :param element: 元素,{'element': 'xxx', 'element_name': '设置按钮', 'by': 'id'}
        :param wait_seconds: 等待时间,超过等待时间仍未找到则返回False。
        :param throws_err: 是否抛出异常。
        :param return_list: 是否返回列表。
        :return:
        """
        try:
            start = time.time()
            while time.time() - start <= wait_seconds:
                """
                # TODO
                find_elements_by_xpath早已被find_elements取代,find_element同理。
                但尝试过使用find_element()查找元素,均报错,而find_elements_by_**可查找到元素。
                猜测是与Appium Server版本有关。由于IT限制,没办法安装更新版本的Appium Server。
                可后续尝试。
                """
                result = self.ios_driver.find_elements(element.by, element.path)
                if result:
                    self.logger.info(f'找到iOS元素[{element.name}]。')
                    element.obj = result if return_list else result[0]
                    return element.obj
                time.sleep(0.1)
            raise ValueError('')
        except Exception as e:
            fail_result = [] if return_list else None
            Universal.return_fail(throws_err, e, fail_result, log_text=f'未查找到iOS元素[{element.to_str()}]: '
                                                                       f'{e.__str__()}')

    @context_listening
    @loop()
    def click_by_ios(self, element: Element, throws_err: bool = True) -> bool:
        """
        【点击】iOS原生/WebView元素
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            element.obj.click()
            self.logger.info(f'成功点击iOS元素[{element.name}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'查找iOS原生元素时({element.to_str()})'
                                                        f'出现意外问题(可根据实际情况忽略):{e.__str__()}。')

    @context_listening
    @loop()
    def input_by_ios(self, element: Element, throws_err: bool = True) -> bool:
        """
        执行【输入】动作,点击成功返回True,否则返回False。
        :param element: 元素,{'element': xxx, 'element_name': '登录按钮', 'text': '输入内容'}。
        :param throws_err: 是否抛出异常。
        :return: bool
        """
        try:
            text = element.input_text
            obj = element.obj
            obj.click()
            orig_text = obj.get_attribute('value')
            text_length = len(orig_text)
            """
            # TODO
            目前这种删除元素文本的方式相对不够优雅。还未找到更好的清除文本的方法。
            """
            if text_length > 0:
                counts = 0
                while counts <= text_length:
                    obj.send_keys(Keys.BACK_SPACE)
                    counts += 1
            obj.send_keys(text)
            self.logger.info(f'[{element.name}]成功输入[{text}]。')
            element.result = True
            return element.result
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'输入文本失败[{element.to_str()}]:{e.__str__()}')

    @context_listening
    @loop()
    def get_text_by_ios(self, element: Element, throws_err: bool = True) -> (str, int, float):
        """
        获取APP UI上的文本
        :param element: 元素,{'element': xxx, 'element_name': '登录输入框', 'get_method': 'text'}。
        :param throws_err: 是否抛出异常。
        :return:
        """
        text = element.obj.get_attribute('value')
        if text is not None:
            self.logger.info(f'成功获取[{element.name}]的文本内容:[{text}]。')
            element.result = text
            return element.result
        return Universal.return_fail(throws_err, ValueError(), log_text=f'输入文本失败[{element.to_str()}]')

    def back_by_ios(self, throws_err=True):
        try:
            VarRepo.ios_orig_driver.switch_context(2)
            Universal.wait(1)
            self.ios_driver.back()
            self.logger.info("成功返回上级页面")
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'ios返回失败:{e.__str__()}')

    def to_app_index_by_ios(self, throws_err=True):
        try:
            VarRepo.ios_orig_driver.switch_context(2)
            Universal.wait(1)
            self.ios_driver.get(VarRepo.app_index_url)
            self.logger.info("成功返回首页")
        except Exception as e:
            Universal.return_fail(throws_err, e, False, f'通过ios返回首页失败:{e.__str__()}')

5.4 page_object\

class DeviceDetails(AndroidOrigBasePage, WebViewBasePage):
    # 音量控制元素(Android原生)
    silence_locator = '{}:id/iv_silence'
    # 设备详情页左上角的左箭头(Android原生)
    arrow_left_locator = '{}:id/iv_back'
    # 设备详情页左上角的左箭头
    arrow_left_xpath_locator = '//i[@type="arrow-left"]/..'
    # 设备详情:console日志的按钮
    log_xpath_locator = '//div[contains(text(),"vConsole")]'
    # 设备详情页面:灯的开关
    power_on_off_locator = '//div[contains(@class,"power-switch")]'
    # 亮度的进度条
    light_box_locator = '//div[contains(@class,"LdsTabs-pane-wrap-active")]//i[contains(@class,' \
                        '"AiDotIcon-bright-less")]/..//div[contains(@class,"LdsSlider-outer")]'
    light_handle_locator = '//div[contains(@class,"LdsTabs-pane-wrap-active")]//i[contains(@class,' \
                           '"AiDotIcon-bright-less")]/..//div[contains(@class,"LdsSlider-handleInner")]'

    def __init__(self, android_orig_driver: u2.Device, web_view_driver: WebDriver):
        """
        继承Android 原生操作驱动类及Android WebView操作驱动类。
        有关APP登录界面的业务操作封装。
        :param android_orig_driver: Android 原生操作驱动
        :param web_view_driver: Android WebView操作驱动
        """
        AndroidOrigBasePage.__init__(self, android_orig_driver)
        WebViewBasePage.__init__(self, web_view_driver)
        self._logger = Log().logger
        self._to_element = Universal.to_element

    @allure.step('查找IPC视频流界面的音量验证视频流首帧')
    def find_video_silence(self) -> any:
        """
        查找IPC播放界面的音量控制元素。
        :return:
        """
        # 可在调用函数时指定循环装饰器的参数,如retry=0。(要保证装饰器参数及被装饰的函数的形参不出现重名)
        return self.find_by_android_orig(self._to_element(self.silence_locator.format(VarRepo.app.app_package),
                                                          '音量按钮', 'id'), wait_seconds=30, retry=0)

``
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容