从企业实战中学习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.
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'},
})
@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类
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():
return
except (NoSuchElementException, StaleElementReferenceException):
return
time.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()):
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.自动化场景稳定性处理(关键保障)
常因为“弹窗干扰”“加载延迟失败”——超时控制/弹窗拦截/进度条等待等
` 装饰器
标记测试步骤,生成allure报告,便于问题追溯
3.
4.
5.设计亮点