从企业实战中学习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():returnassert self.controls['msgbox内容'].text.startswith('刺激输出已被关闭')self.controls['确定按钮'].click()def confirm_change_stim(self):if not self.controls['msgbox内容'].exist():returnassert '确定切换到' in self.controls['msgbox内容'].textself.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():returncontent = self.controls['msgbox内容'].textassert ('是否要切换刺激模式?\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()@classmethoddef 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.
3.
为什么大部分方法是实例方法?
答:因为在消息对话框处理类中,每个消息对话框都是在不同业务场景下的,需要通过实例提前绑定具体的业务场景
二.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'},})@classmethoddef handle_line_busy(cls, app) -> bool:time.sleep(1)tip = CommonMsgTip(app)if not tip.controls['弹窗提示语'].exist():return Falsetry:if tip.controls['弹窗提示语'].exist() and tip.controls['弹窗提示语'].text == '通讯线路忙,请稍候再试':tip.controls['弹窗关闭按钮'].click()return Trueexcept Exception:return Falsereturn False@classmethoddef handle_offline(cls, app) -> bool:time.sleep(1)msg_tip = CommonMsgTip(app)if not msg_tip.controls['弹窗提示语'].exist():return Falsetry:if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '连接信号已断开,请调整角度或移近距离后重试':msg_tip.controls['弹窗关闭按钮'].click()return Trueexcept Exception:return Falsereturn False@classmethoddef handle_exception(cls, app) -> bool:time.sleep(1)msg_tip = CommonMsgTip(app)if not msg_tip.controls['弹窗提示语'].exist():return Falsetry:if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '':msg_tip.controls['弹窗关闭按钮'].click()return Trueexcept Exception:return Falsereturn False@classmethoddef handle_low_battery(cls, app) -> bool:time.sleep(1)msg_tip = CommonMsgTip(app)if not msg_tip.controls['弹窗提示语'].exist():return Falsetry:# print(msg_tip.controls['弹窗提示语'].text)if msg_tip.controls['弹窗提示语'].exist() and msg_tip.controls['弹窗提示语'].text == '刺激器电量不足,无法开启程控哦!':msg_tip.controls['弹窗关闭按钮'].click()return Trueexcept Exception:return Falsereturn False
(二)大概
1.po模式的“专项化延伸”:弹窗专属PO类
2.类方法(@classmethod)的妙用:无需实例化,直接调用
3.
4.
5.
三.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():returnexcept (NoSuchElementException, StaleElementReferenceException):returntime.sleep(wait_interval)raise TimeoutError(f'等待控件消失超时:控件[{control_key}]:[{value}]依然存在')
(一)知识点!!
1.为什么wait_activity为false?设为true可以吗
这个工具类的作用是:在当前已显示的页面中找加载状态控件
(1)每个页面的加载状态控件都不一样啊,怎么确定初始化的加载动态控件一定是Ipg连接界面的呢?
(2)CommonLoadingBar初始化谁?它怎么知道当前屏幕上显示的页面是IPG连接页面?
2.wait_for_loading_disappear
不使用@classmethod
装饰器,不设计为类方法?而CommonMsgTip类里的方法却设计为类方法
而实例方法能通过 “创建实例” 提前绑定目标;类方法只能 “每次临时找目标”,很容易盯错或盯一半就断了 —— 所以必须用实例方法。?
3.怎么绑定到Ipg连接界面的, loading_bar = CommonLoadingBar(self.get_app())这个绑定的不是只是app吗?
——————————————————-——————————————————————
(二)总结!
1.在页面类中,你是怎么去判断一个方法是适合设计为类方法还是实例方法?
答:这个方法中所操作的元素是跨页面的公共元素(比如提示弹窗),还是和特定场景强关联的元素
元素的通用性和定位稳定性决定了方法类型。
通用且定位稳定的元素适合类方法简化调用;与具体场景强关联的元素需要实例方法绑定上下文,确保操作准确。
2.什么样的类适合将wait_activity设定为false或ture?
答:看这个类是工具类(不对应独立页面,仅监控/操作当前屏幕的临时元素)还是页面业务类(对应app中一个独立的页面,有明确的activity标识)
四.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()):breakloading_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()):breakloading_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 Falseif CommonMsgTip.handle_offline(self.get_app()):return Falsemsg_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)continueelse:return Truereturn False# 搜索IPG@allure.step('点击搜索IPG按钮搜索IPG;')def search_ipg(self, ipg) -> bool:# allure.step(f"ipg编号: {ipg},连接该ipg")self.controls['ipg搜索输入框'].text = ipgself.controls['ipg搜索输入框'].hide_keyboard()timeout = 60 * 5time0 = time.time()while time.time() - time0 < timeout:self.stop_find_ipg()self.controls['ipg搜索按钮'].click()if CommonMsgTip.handle_line_busy(self.get_app()):continueif CommonMsgTip.handle_offline(self.get_app()):continueif CommonMsgTip.handle_exception(self.get_app()):continuetime.sleep(2)by, value = self.get_locator('ipg序列号')view_list = self.controls['ipg搜索列表'].children(by, value)view = Nonefor sr_view in view_list:if sr_view.text.startswith(ipg):view = sr_viewbreakif view:view.click()if self._handle_process_bar():return Truereturn False# raise ControlNotFoundError(f"连接体外程控器: {ipg}失败,未找到对应体外程控器")def find_ipg(self, ipg, name: str = ''):for i in range(0, 5):timeout = 60time0 = 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_viewif sr_view.text.startswith(ipg):self.stop_find_ipg()return sr_viewtime.sleep(2)self.controls['ipg搜索列表'].swipe_up()self.stop_find_ipg()return Nonedef connect_ipg(self, ipg_view: SRView, timeout=60) -> bool:if not ipg_view:return Falsetime0 = time.time()while time.time() - time0 < timeout:ipg_view.click()if CommonMsgTip.handle_line_busy(self.get_app()):continueif CommonMsgTip.handle_offline(self.get_app()):continueif CommonMsgTip.handle_exception(self.get_app()):continueif self._handle_process_bar():return Truereturn False
(一)代码讲解
1.自动化场景稳定性处理(关键保障)
常因为“弹窗干扰”“加载延迟失败”——超时控制/弹窗拦截/进度条等待等
2.
@allure.step
装饰器标记测试步骤,生成allure报告,便于问题追溯
3.
4.
5.设计亮点