Posted by Liler on July 20, 2022
在对Android平台上的APP进行自动化时,经常遇到列表,例如菜单列表、信息流列表等。 为什么大部分信息用列表的形式出现呢?我想这个手机的屏幕有关,手机屏幕基本上又小 又细长,为了让信息适应这样的屏幕,用列表显示信息就正常不过了。
既然APP里面的信息大部分都是通过列表显示的,那么对APP进行自动化时就必须考虑怎么自动化 列表了。显然自动化操作列表对于自动化APP是比较重要的,大部分时间都是操作列表。
那么怎样对列表进行操作呢?仔细观察后,你就会知道列表有两种形式: 纵向列表 与 横向列表 。 纵向列表比较常见,大部分比较多的信息以这种形式展示。横向列表与纵向列表除了方向上面的 不同外,其他基本上是一样的,本文只关注纵向列表。
这些列表在本文中统称为 数据列表 (Datalist).
看下纵向列表的图片(以Twitter为例):
这些纵向列表有什么共同特征呢,有什么共同操作呢? 为了了解这些信息,我们用 appium-inspector 来解析页面。
首先来看这些页面包含哪些部分:
通过观察这些图片我们可以知道列表主要包含下面的几个部分:
列表根元素 (root element)
根元素定义了列表的最大显示区域及其内容。注意上面的最大显示区域的最上面 被Tabs bar元素掩盖了一部分。有些最大显示区域没有被掩盖,有些则被掩盖了 (有的在最上面,有的在最下面)。
列表最外层子元素 (outermost element)
这个定义了子元素的显示区域及其内容。有些会包含多个子元素,例如Tweet最外层 子元素除了Tweet内容外,还包含统计信息的显示条。
列表最内层子元素 (innermost element)
这个子元素也叫内容根元素(content root element),定义了信息的显示区域及其内容。 这个元素下面还可能包含多个子信息元素。
一屏信息列表
顾名思义,就是在某个时间点的一屏幕所显示的所有信息元素。从上到下,可以分为 第一个子元素,第二个子元素,...
第一个子元素或者最后一个子元素都有可能被其他元素遮盖。
列表掩盖元素 (列表遮盖元素)(cover element)
这个元素在列表的最上面或者最下面,它的显示区域与列表根元素的显示区域有重叠。
那么对这些元素或者组成部分进行哪些操作呢?
获取根元素及其显示区域
获取所有的最外层子元素
获取所有的最内层子元素
定义掩盖元素的位置与获取掩盖元素的显示区域
计算去掉遮盖元素显示区域的列表可视显示区域(列表有效显示区域)(列表视窗) (View window)
判断元素是否被部分或全部遮盖
获取元素的文本内容
滑动元素(向上或者向下)
打开某个元素(点击某个元素)(需要定义点击在元素的哪个位置)
一边滑动列表一边做某种操作(重要的操作,毕竟操作列表就是为了做某些操作的)
一边滑动列表一边打开某个元素一边做某种操作(重要的操作)
...
还有一些其他共同操作就不一一列举了。请查看下面的一些代码,可以了解有哪些共同的操作。
Datalist实现代码片段(Datalist类的配置代码):
import random import time try: from appium.webdriver.common.appiumby import AppiumBy as By except ModuleNotFoundError: from appium.webdriver.common.mobileby import MobileBy as By from collections.abc import Iterable from .widget import Widget from utils.common import is_cmd class Datalist(Widget): # list root element root_element_xpath = '' root_element_locators = () # list item element outermost_item_element_xpath = f'{root_element_xpath}/*' outermost_item_element_locators = ( (By.XPATH, outermost_item_element_xpath), ) innermost_item_element_locators = () # list content element content_root_element_locators = innermost_item_element_locators # View window cover element # If locatos of it is not blank, but cannot find any element, then ignore cover element # in order to be compatible with this type list which has no cover element cover_element_locators = () cover_element_position = 'bottom' # or top cover_element_fixed = True # if the the view port is changed at the beginning of swipe, # the swipe will affect the postion and height of view port, # then should get the view port every time is_view_port_changed = False # There are some type of elements whick cannot be swiped skip_elements_locators = [] pname = 'Data list' tap_duration = 400 duration = 4000 x_delta = 100 y_delta = 2 slide_times = 10 # If the length between start_y and end_y is less or equal to the value, then skip this element skip_length = 50 item_element_level = 'outermost' # or innermost, for getting item element
Datalist类的方法与变量:
▾+Datalist : class +__init__(self, driver, logger=None, timeout=None) : member +get_attribute_of_content_element_from_root(self, atrribute, locators, content_root_element, ename=None) : member +get_content_element_from_root(self, locators, content_root_element, ename=None) : member +get_content_elements_from_root(self, locators, content_root_element, ename=None) : member +get_content_root_elements(self) : member +get_content_root_elements_in_view_window(self, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_cover_element(self) : member +get_cover_element_dimensions(self) : member +get_elements_id_of_skip_item_by_locators(self) : member +get_first_item_text(self) : member +get_innermost_elements(self) : member +get_innermost_elements_in_view_window(self, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_item_element_dimensions_in_view_window(self, item_element) : member +get_item_elements_in_view_window(self, element_level=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_list_view_window(self, root_element=None) : member +get_one_item_all_text(self, text_type='text', index=0, element_level=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_one_item_element_in_view_window(self, index=0, element_level=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_outermost_elements(self, timeout=None, post_cmd=None) : member +get_outermost_elements_in_view_window(self, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +get_root_element(self) : member +get_root_element_window(self, root_element=None) : member ▸+is_item_element_display(self, item_element) : member +is_skip_item_by_length_from_view_window(self, item_element, skip_length=None) : member +is_skip_item_by_locators(self, item_element, skip_elements_ids) : member +tap_first_item_in_view_window(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +tap_item_element(self, item_element, position='left', duration=None, x_delta=None, y_delta=None) : member +tap_last_item_in_view_window(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +tap_one_item_in_view_window(self, index=0, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member [variables] +content_root_element_locators +cover_element_fixed +cover_element_locators +cover_element_position +duration +innermost_item_element_locators +is_view_port_changed +item_element_level +outermost_item_element_locators +outermost_item_element_xpath +pname +root_element_locators +root_element_xpath +skip_elements_locators +skip_length +slide_times +tap_duration +x_delta +y_delta
VerticalDatalist实现代码片段(VerticalDatalist类的配置代码):
class VerticalDatalist(Datalist): # list root element root_element_xpath = '' root_element_locators = () # list item element outermost_item_element_xpath = f'{root_element_xpath}/*' outermost_item_element_locators = ( (By.XPATH, outermost_item_element_xpath), ) innermost_item_element_locators = () # list content element content_root_element_locators = innermost_item_element_locators # View window cover element # If locatos of it is not blank, but cannot find any element, then ignore cover element # in order to be compatible with this type list which has no cover element cover_element_locators = () cover_element_position = 'bottom' # or top cover_element_fixed = True # if the the view port is changed at the beginning of swipe, # the swipe will affect the postion and height of view port, # then should get the view port every time is_view_port_changed = False # There are some type of elements whick cannot be swiped skip_elements_locators = [] pname = 'Vertical Data list' tap_duration = 400 duration = 4000 x_delta = 100 y_delta = 2 slide_times = 10 # If the length between start_y and end_y is less or equal to the value, then skip this element skip_length = 50 item_element_level = 'outermost' # or innermost, for getting item element
VerticalDatalist类的方法与变量:
▾+VerticalDatalist : class +__init__(self, driver, logger=None, timeout=None) : member +find_and_click_element_by_text(self, text, locators, results=None, is_case_sensisive=False) : member +find_element_by_text(self, text, locators, results=None, is_case_sensisive=False) : member +flick_item_vertically(self, item_element, position='left', swipe_from='top', swipe_to='top', x_delta=None, y_delta=None, skip_length=None) : member +get_element_from_elements(self, elements, locators, text, is_case_sensisive=False) : member +get_text_from_elements(self, elements, locators) : member +random_slide_list(self, slide_times=None, position='left', swipe_from_top_chance=5, swipe_from_bottom_chance=5, swipe_to_top_chance=10, swipe_to_bottom_chance=5, swipe_to='top', item_index=None, duration=None, x_delta=None, y_delta=None, skip_length=None, cmd=None, cmd_kwargs=None, cmd_results=None, is_check_cmd_result=False, is_fix_swip_bug=True, is_random_item_index=False, slide_method_swipe_chance=5, slide_method_flick_chance=5, when_run_cmd='before', is_sleep_after_slide=False, sleep_time=None) : member +slide_item_vertically(self, item_element, position='left', swipe_from='top', swipe_to='top', duration=None, x_delta=None, y_delta=None, skip_length=None, slide_method='swipe') : member +slide_list(self, slide_times=None, position='left', swipe_from='top', swipe_to='top', item_index=None, duration=None, x_delta=None, y_delta=None, skip_length=None, cmd=None, cmd_kwargs=None, cmd_results=None, is_check_cmd_result=False, is_fix_swip_bug=True, is_random_item_index=False, slide_method='swipe', when_run_cmd='before', is_sleep_after_slide=False, sleep_time=None, element_level=None, is_cmd_change_page=False, require_no_elements=False, is_fix_no_moving=True) : member +swipe_first_item_bottom_to_bottom(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +swipe_first_item_bottom_to_top(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +swipe_first_item_top_to_bottom(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +swipe_item_bottom_to_bottom(self, item_element, position='left', duration=None, x_delta=None, y_delta=None, skip_length=None) : member +swipe_item_bottom_to_top(self, item_element, position='left', duration=None, x_delta=None, y_delta=None, skip_length=None) : member +swipe_item_top_to_bottom(self, item_element, position='left', duration=None, x_delta=None, y_delta=None, skip_length=None) : member +swipe_item_top_to_top(self, item_element, position='left', duration=None, x_delta=None, y_delta=None, skip_length=None) : member +swipe_item_vertically(self, item_element, position='left', swipe_from='top', swipe_to='top', duration=None, x_delta=None, y_delta=None, skip_length=None) : member +swipe_last_item_bottom_to_top(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +swipe_last_item_top_to_bottom(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member +swipe_last_item_top_to_top(self, position='left', duration=None, x_delta=None, y_delta=None, is_check_skip_length=False, skip_length=None, is_check_skip_locators=False) : member [variables] +content_root_element_locators +cover_element_fixed +cover_element_locators +cover_element_position +duration +innermost_item_element_locators +is_view_port_changed +item_element_level +outermost_item_element_locators +outermost_item_element_xpath +pname +root_element_locators +root_element_xpath +skip_elements_locators +skip_length +slide_times +tap_duration +x_delta +y_delta
本文关注的是怎么设计自动化操作Android平台上的列表,不关注怎么具体实现。 实际上根据上面的分析与我的实现代码框架,你可以写出属于你的实现代码。