目录

从企业实战中学习Appium自动化二

从企业实战中学习Appium自动化(二)

一.CommonMsgBox

通用消息对话框处理类


'''
消息对话框处理
'''
class CommonMsgBox(SRActivity):

    def __init__(self, sr_app: SR1620App):
        super().__init__(sr_app, wait_activity=False)
        self.update_locator({
            #  通用按钮
            'msgbox': {BY: SRBy.ID, VALUE: 'android:id/content'},
            'msgbox_title': {BY: SRBy.ID, VALUE: 'id/txt_dialog_title'},
            'msgbox内容': {BY: SRBy.ID, VALUE: 'id/txt_dialog_content'},
            '取消按钮': {BY: SRBy.ID, VALUE: 'id/txt_negative_btn'},
            '确定按钮': {BY: SRBy.ID, VALUE: 'id/txt_positive_btn'},
            #重命名输入框/修改刺激程序名字
            '重命名输入框': {BY: SRBy.ID, VALUE: 'id/et_rename'},
            #  定时刺激页面的“同步时间”弹窗
            '当前PAD时间': {BY: SRBy.ID, VALUE: 'id/txt_pad_time'},
            '当前IPG时间': {BY: SRBy.ID, VALUE: 'id/txt_ipg_time'},
            # 配对页面的“配对”弹窗
            'WLR信息': {BY: SRBy.ID, VALUE: 'id/txt_tlm_sn'},
            'IPG信息': {BY: SRBy.ID, VALUE: 'id/txt_ipg_sn'},
            #  IPG校验控件
            'code1': {BY: SRBy.ID, VALUE: 'id/tv_code1'},
            'code2': {BY: SRBy.ID, VALUE: 'id/tv_code2'},
            'code3': {BY: SRBy.ID, VALUE: 'id/tv_code3'},
            'code4': {BY: SRBy.ID, VALUE: 'id/tv_code4'},

        })

    def confirm_open_stim(self):
        if not self.controls['msgbox内容'].exist():
            return
        assert self.controls['msgbox内容'].text.startswith('刺激输出已被关闭')
       
        self.controls['确定按钮'].click()

    def confirm_change_stim(self):
        if not self.controls['msgbox内容'].exist():
            return
        assert '确定切换到' in self.controls['msgbox内容'].text
        self.controls['确定按钮'].click()

    def confirm_close_connection(self):
        assert self.controls['msgbox内容'].text.startswith('确定断开刺激连接')      
        self.controls['确定按钮'].click()

    def confirm_delete_program(self):
        assert self.controls['msgbox_title'].text == '删除程序'
        self.controls['确定按钮'].click()

    def confirm_change_stim_mode(self):
        if not self.controls['msgbox内容'].exist():
            return
        content = self.controls['msgbox内容'].text
        assert ('是否要切换刺激模式?\n 确定后,当前标准程序会被清空' == content)
        self.controls['确定按钮'].click()

    def confirm_sync_time(self):
        assert self.controls['msgbox_title'].text == '同步时间'
        self.controls['确定按钮'].click()

    def confirm_pair_ipg(self, ipg):
        assert self.controls['msgbox_title'].text == '配对提醒'
        assert self.controls['IPG信息'].text.startswith(f'患者IPG序列号:{ipg}')
        self.controls['确定按钮'].click()

    def confirm_dis_pair_ipg(self):
        assert self.controls['msgbox_title'].text == '解除配对'
        self.controls['确定按钮'].click()

    def confirm_conn_ipg(self, ipg):
        if self.controls['msgbox_title'].exist() and self.controls['msgbox_title'].text == '校验':
            self.controls['code1'].press_text(int(ipg[-4:-3]))
            self.controls['code2'].press_text(int(ipg[-3:-2]))
            self.controls['code3'].press_text(int(ipg[-2:-1]))
            self.controls['code4'].press_text(int(ipg[-1:]))
            self.controls['确定按钮'].click()

    @classmethod
    def msg_box_exist(cls, app) -> bool:
        msg_box = CommonMsgBox(app)
        return msg_box.controls['msgbox'].exist() and msg_box.controls['msgbox_title'].exist()

    def confirm_over_charge_density_limit(self):
        assert self.controls['msgbox_title'].text == '温馨提示'
        assert self.controls['msgbox内容'].text == '刺激参数的电荷密度越限,是否继续程控?'
        self.controls['确定按钮'].click()

    def confirm_set_p1_zero(self):
        assert self.controls['msgbox_title'].text == '温馨提示'
        assert self.controls['msgbox内容'].text == '清空P1幅值将会使P2幅值归零'
        self.controls['确定按钮'].click()

(一)大概

1.通用元素/特定场景元素

2.

https://i-blog.csdnimg.cn/direct/bf5d088724e1484ab92b942352317d76.png

3.

https://i-blog.csdnimg.cn/direct/ab6abdb0462b486f92430ed61b3d1e6a.png

为什么大部分方法是实例方法?

答:因为在消息对话框处理类中,每个消息对话框都是在不同业务场景下的,需要通过实例提前绑定具体的业务场景

二.CommonMsgTip

通用提示弹窗处理


class CommonMsgTip(SRActivity):

    def __init__(self, sr_app: SR1620App):
        super(CommonMsgTip, self).__init__(sr_app, wait_activity=False)
        self.update_locator({
            # 提示弹窗
            '提示弹窗': {BY: SRBy.ID, VALUE: 'id/ll_prompt'},
            '弹窗内容': {BY: SRBy.ID, VALUE: 'id/dialog_content'},
            '弹窗提示语': {BY: SRBy.ID, VALUE: 'id/tv_tip'},
            '弹窗关闭按钮': {BY: SRBy.ID, VALUE: 'id/img_close'},
        })

    @classmethod
    def handle_line_busy(cls, app) -> bool:
        time.sleep(1)
        tip = CommonMsgTip(app)
        if not tip.controls['弹窗提示语'].exist():
            return False
        try:
            if tip.controls['弹窗提示语'].exist() and tip.controls['弹窗提示语'].text == '通讯线路忙,请稍候再试':
                tip.controls['弹窗关闭按钮'].click()
                return True
        except Exception:
            return False
        return False

    @classmethod
    def handle_offline(cls, app) -> bool:
        time.sleep(1)
        msg_tip = CommonMsgTip(app)
        if not msg_tip.controls['弹窗提示语'].exist():
            return False
        try:
            if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '连接信号已断开,请调整角度或移近距离后重试':
                msg_tip.controls['弹窗关闭按钮'].click()
                return True
        except Exception:
            return False
        return False

    @classmethod
    def handle_exception(cls, app) -> bool:
        time.sleep(1)
        msg_tip = CommonMsgTip(app)
        if not msg_tip.controls['弹窗提示语'].exist():
            return False
        try:
            if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '':
                msg_tip.controls['弹窗关闭按钮'].click()
                return True
        except Exception:
            return False
        return False

    @classmethod
    def handle_low_battery(cls, app) -> bool:
        time.sleep(1)
        msg_tip = CommonMsgTip(app)
        if not msg_tip.controls['弹窗提示语'].exist():
            return False
        try:
            # print(msg_tip.controls['弹窗提示语'].text)
            if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '刺激器电量不足,无法开启程控哦!':
                msg_tip.controls['弹窗关闭按钮'].click()
                return True
        except Exception:
            return False
        return False

(二)大概

1.po模式的“专项化延伸”:弹窗专属PO类

https://i-blog.csdnimg.cn/direct/4d7547ba16bd455aadf132d268d5e373.png

2.类方法(@classmethod)的妙用:无需实例化,直接调用

https://i-blog.csdnimg.cn/direct/be0862cb80014a7eb46a3968ad9bc776.png

3.

https://i-blog.csdnimg.cn/direct/1ee8f4a8c20848f9bd5aa28d1cc1d982.png

4.

https://i-blog.csdnimg.cn/direct/fe2c02ec1910444c955f6c86d2dc838f.png

5.

https://i-blog.csdnimg.cn/direct/0cc8940b3b3e4c8ea0460ba1aea844e3.png

三.CommonLoadingBar

转圈的小图标——“加载状态”


class CommonLoadingBar(SRActivity):
    def __init__(self, sr_app: SR1620App):
        super().__init__(sr_app, wait_activity=False) # 禁用“页面Activity等待”
        self.update_locator({
            # 提示弹窗
            'loading状态': {BY: SRBy.ID, VALUE: 'id/img_loading'},
            'loading状态2': {BY: SRBy.ID, VALUE: 'id/ivProgress'},
        })
    
    def wait_for_loading_disappear(self, timeout=5):
        self.wait_for_disappear('loading状态', wait_timeout=timeout)
        self.wait_for_disappear('loading状态2', wait_timeout=timeout)

class SRActivity(object):
      .....
      .....
      .....
      def wait_for_disappear(self, control_key: str, wait_timeout=30, wait_interval=0.5):
        '''等待控件消失,比如loading动画控件等
        :param control_key:
        :param wait_timeout:
        :param wait_interval:
        :return:
        '''
        by, value = self.get_locator(control_key)
        time0 = time.time()
        while time.time() - time0 < wait_timeout:
            try:
                view = self._driver.find_element(by, value)
                if not view.is_enabled() or not view.is_displayed():
                    return
            except (NoSuchElementException, StaleElementReferenceException):
                return
            time.sleep(wait_interval)
        raise TimeoutError(f'等待控件消失超时:控件[{control_key}]:[{value}]依然存在')

(一)知识点!!

1.为什么wait_activity为false?设为true可以吗

这个工具类的作用是:在当前已显示的页面中找加载状态控件

https://i-blog.csdnimg.cn/direct/809a1c014fd748898862f0cb75aec04c.png

(1)每个页面的加载状态控件都不一样啊,怎么确定初始化的加载动态控件一定是Ipg连接界面的呢?

https://i-blog.csdnimg.cn/direct/1db2f39df8504d8ca162333738390d19.png

https://i-blog.csdnimg.cn/direct/6e8f73adb339465fa2815deef6d00a7a.png

https://i-blog.csdnimg.cn/direct/4a31be8464ad4009bd41aefd731ccd53.png

(2)CommonLoadingBar初始化谁?它怎么知道当前屏幕上显示的页面是IPG连接页面?

https://i-blog.csdnimg.cn/direct/086ad5a571244cc7ac651052c7c9f892.png

https://i-blog.csdnimg.cn/direct/f1548cbf63cd49fc8e37f3b7be03458d.png

https://i-blog.csdnimg.cn/direct/658fca3645744155a06ef34524f28ae6.png

2.wait_for_loading_disappear 不使用 @classmethod 装饰器,不设计为类方法?而CommonMsgTip类里的方法却设计为类方法

https://i-blog.csdnimg.cn/direct/6bf695211bb24357a9c96a3c112d1b06.png

而实例方法能通过 “创建实例” 提前绑定目标;类方法只能 “每次临时找目标”,很容易盯错或盯一半就断了 —— 所以必须用实例方法。?

https://i-blog.csdnimg.cn/direct/51bf2e150aad456385ded8e094d2c4b8.png

https://i-blog.csdnimg.cn/direct/b375d58a3fbc406b8f0bc0ddce066d39.png

https://i-blog.csdnimg.cn/direct/70f84ab946aa47b9971cc3e2a256e6a9.png

https://i-blog.csdnimg.cn/direct/0ed7273728464f08af90169baf76ad3d.png

3.怎么绑定到Ipg连接界面的, loading_bar = CommonLoadingBar(self.get_app())这个绑定的不是只是app吗?

https://i-blog.csdnimg.cn/direct/391642344d244edea3000f2b73b3ffd5.png

https://i-blog.csdnimg.cn/direct/52d327a19cbf4200a6f47159b545acd8.png

https://i-blog.csdnimg.cn/direct/582c763bbb9d4172b38f2e9cf4387b41.png

——————————————————-——————————————————————

https://i-blog.csdnimg.cn/direct/5359e9d9a47a40d4959977da74fb97c7.png

https://i-blog.csdnimg.cn/direct/20f70250e0434a83969f4440d14af2bc.png

(二)总结!

1.在页面类中,你是怎么去判断一个方法是适合设计为类方法还是实例方法?

答:这个方法中所操作的元素是跨页面的公共元素(比如提示弹窗),还是和特定场景强关联的元素

元素的通用性和定位稳定性决定了方法类型

通用且定位稳定的元素适合类方法简化调用;与具体场景强关联的元素需要实例方法绑定上下文,确保操作准确。

https://i-blog.csdnimg.cn/direct/d3fadb8f08124d889755bce30a26dd30.png

2.什么样的类适合将wait_activity设定为false或ture?

答:看这个类是工具类(不对应独立页面,仅监控/操作当前屏幕的临时元素)还是页面业务类(对应app中一个独立的页面,有明确的activity标识)

https://i-blog.csdnimg.cn/direct/8094ec4d3d9d45e881a155be11340291.png

四.IPGPage





class IPGConfirmPage(SRActivity):

    def __init__(self, sr_app: SR1620App):
        super().__init__(sr_app)
        self.update_locator({

        })


class IPGPage(IPGConfirmPage):
    Activity = '.modules.connectipg.ActConnectIPG'  # 连接IPG界面

    def __init__(self, sr_app: SR1620App):
        super().__init__(sr_app)
        self.update_locator({
            '体外程控器编号': {'by': SRBy.ID, 'value': 'id/tv_tlm_info'},
            '程控记录按钮': {'by': SRBy.ID, 'value': 'id/frame_record'},
            '配对按钮': {'by': SRBy.ID, 'value': 'id/frame_pair'},
            '设置按钮': {'by': SRBy.ID, 'value': 'id/frame_setting'},
            '返回按钮': {'by': SRBy.ID, 'value': 'id/iv_back'},
            'ipg搜索输入框': {'by': SRBy.ID, 'value': 'id/et_search'},
            'ipg搜索按钮': {'by': SRBy.ID, 'value': 'id/iv_search'},
            'ipg搜索列表': {'by': SRBy.ID, 'value': 'id/rv_ipg_list'},
            'ipg序列号': {'by': SRBy.XPATH, 'value': '(.//android.widget.TextView)'},
            '查找按钮': {'by': SRBy.ID, 'value': 'id/tv_scan'},
            #  注意
            '注意提示语': {'by': SRBy.ANDROID_UIAUTOMATOR, 'value': 'new UiSelector().text("注意:请在患者前方1米范围内搜寻并连接")'},

        })

    @allure.step('点击查找按钮,进行查找IPG;')
    def begin_find_ipg(self):
        while self.controls['查找按钮'].text == '查找':
            self.controls['查找按钮'].click()
            time.sleep(1)
            if not CommonMsgTip.handle_line_busy(self.get_app()):
                break
        loading_bar = CommonLoadingBar(self.get_app())
        loading_bar.wait_for_loading_disappear(10)

    def stop_find_ipg(self):
        while self.controls['查找按钮'].text == '停止查找':
            self.controls['查找按钮'].click()
            time.sleep(1)
            if not CommonMsgTip.handle_line_busy(self.get_app()):
                break
        loading_bar = CommonLoadingBar(self.get_app())
        loading_bar.wait_for_loading_disappear(10)


    def _handle_process_bar(self, timeout=120) -> bool:
        time.sleep(1)
        time0 = time.time()
        while time.time() - time0 < timeout:
            if CommonMsgTip.handle_line_busy(self.get_app()):
                return False
            if CommonMsgTip.handle_offline(self.get_app()):
                return False
            msg_box = CommonMsgBox(self.get_app())
            msg_box.confirm_conn_ipg(self.get_app().get_ipg())
            msg_tip = CommonMsgTip(self.get_app())
            if msg_tip.controls['弹窗提示语'].exist():
                time.sleep(1)
                continue
            else:
                return True
        return False

    # 搜索IPG
    @allure.step('点击搜索IPG按钮搜索IPG;')
    def search_ipg(self, ipg) -> bool:
        # allure.step(f"ipg编号: {ipg},连接该ipg")
        self.controls['ipg搜索输入框'].text = ipg
        self.controls['ipg搜索输入框'].hide_keyboard()
        timeout = 60 * 5
        time0 = time.time()
        while time.time() - time0 < timeout:
            self.stop_find_ipg()
            self.controls['ipg搜索按钮'].click()
            if CommonMsgTip.handle_line_busy(self.get_app()):
                continue
            if CommonMsgTip.handle_offline(self.get_app()):
                continue
            if CommonMsgTip.handle_exception(self.get_app()):
                continue
            time.sleep(2)
            by, value = self.get_locator('ipg序列号')
            view_list = self.controls['ipg搜索列表'].children(by, value)
            view = None
            for sr_view in view_list:
                if sr_view.text.startswith(ipg):
                    view = sr_view
                    break
            if view:
                view.click()
                if self._handle_process_bar():
                    return True
        return False
        # raise ControlNotFoundError(f"连接体外程控器: {ipg}失败,未找到对应体外程控器")

    def find_ipg(self, ipg, name: str = ''):
        for i in range(0, 5):
            timeout = 60
            time0 = time.time()
            self.begin_find_ipg()
            while time.time() - time0 < timeout:
                by, value = self.get_locator('ipg序列号')
                view_list = self.controls['ipg搜索列表'].children(by, value)
                for sr_view in view_list:
                    if name and sr_view.text == ipg + name:
                        self.stop_find_ipg()
                        return sr_view
                    if sr_view.text.startswith(ipg):
                        self.stop_find_ipg()
                        return sr_view
                time.sleep(2)
                self.controls['ipg搜索列表'].swipe_up()
            self.stop_find_ipg()
        return None


    def connect_ipg(self, ipg_view: SRView, timeout=60) -> bool:
        if not ipg_view:
            return False
        time0 = time.time()
        while time.time() - time0 < timeout:
            ipg_view.click()
            if CommonMsgTip.handle_line_busy(self.get_app()):
                continue
            if CommonMsgTip.handle_offline(self.get_app()):
                continue
            if CommonMsgTip.handle_exception(self.get_app()):
                continue
            if self._handle_process_bar():
                return True
        return False

(一)代码讲解

1.自动化场景稳定性处理(关键保障)

常因为“弹窗干扰”“加载延迟失败”——超时控制/弹窗拦截/进度条等待等

https://i-blog.csdnimg.cn/direct/78bf1097616e4ce787ba698f19b2c795.png

https://i-blog.csdnimg.cn/direct/d227725783d54cb6950c57f0b644eafb.png

` 装饰器

标记测试步骤,生成allure报告,便于问题追溯

https://i-blog.csdnimg.cn/direct/6dde674423a24bf2bd74c888a27e21a1.png

3.

https://i-blog.csdnimg.cn/direct/e4a3e0d146e8471d93bb804428b99ab4.png

4.

https://i-blog.csdnimg.cn/direct/1ec27da893f24b669bda51793b45fddc.png

https://i-blog.csdnimg.cn/direct/acf363c426044fcca870a4aaaff8d9fe.png

5.设计亮点

https://i-blog.csdnimg.cn/direct/36d110eba6544a0793569474458f27e1.png