Macaca Macaca 上滑屏、滑屏查找 element、在指定 element 上下左右指定位置点击

王华林 · 2017年02月25日 · 最后由 zhaolfa 回复于 2017年11月03日 · 2128 次阅读

由于滑屏、滑屏查找 element 的方法在 UI 自动化测试中比较通用,因此,对 Macaca 的手势操作进行了简单封装以简化 UI 自动化用例脚本的编写。加之项目的自身的特点,有些控件的 xpath 经常变化,因此,也封装了 “在指定 element 上下左右指定位置点击” 的方法。具体方法见下方代码。

其中,由于简化实现难度,对很对细节上的把控不是很完善(希望可以和大家讨论出更为合适的解决办法),主要问题如下:
1、对手机屏幕的滑动是从屏幕中心点向四周滑动四分之一屏幕的宽/高
2、对 element 的滑动是从 element 的中心点向四周滑动半个 elemnt 的宽/高
3、滑动查找 element 是通过指定最大滑动次数(默认 5 次,可通过参数设定)来定的。因为目前这种方式能解决问题,所以这个地方目前没有用最完善的方案去解决
4、在指定 element 上下左右指定位置点击中,是在指定的 element 的四个边界 + 给定的 rate 倍数 element 的宽/高处进行点击

用法举例(若需了解 PageObject 工程结构,可见 https://testerhome.com/topics/7550):

class PlatformAppHomePage(BasePage):
    @teststep
    def click_finance_choiceness_more(self):
        """以“理财精选”对应的“更多”的ID为依据"""
        self.find_element_on_vertical('id', 'com.platform.jhj:id/home_welfare_more_tv').click()

    @teststep
    def click_car_insurance(self):
        """点击“车险”Icon的中文字体上方(高出中文字体上边界2倍中文字体的高度)"""
        element = self.driver.element_by_name('车险')
        self.click_above_of_element(element, rate=2)

该类代码如下:

from macaca import WebDriverException

class BasePage(object):
    @classmethod
    def set_driver(cls, dri):
        cls.driver = dri

    def get_driver(self):
        return self.driver

    def _get_window_size(self):
        window = self.driver.get_window_size()
        y = window['height']
        x = window['width']

        return x, y

    @staticmethod
    def _get_element_size(element):
        rect = element.rect

        x_center = rect['x'] + rect['width'] / 2
        y_center = rect['y'] + rect['height'] / 2
        x_left = rect['x']
        y_up = rect['y']
        x_right = rect['x'] + rect['width']
        y_down = rect['y'] + rect['height']

        return x_left, y_up, x_center, y_center, x_right, y_down

    def _swipe(self, fromX, fromY, toX, toY, steps):
        self.driver \
            .touch('drag', {'fromX': fromX, 'fromY': fromY, 'toX': toX, 'toY': toY, 'steps': steps})

    def swipe_up(self, element=None, steps=10):
        """
        swipe up
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :return: None
        """
        if element:
            x_left, y_up, x_center, y_center, x_right, y_down = self._get_element_size(element)

            fromX = x_center
            fromY = y_center
            toX = x_center
            toY = y_up
        else:
            x, y = self._get_window_size()
            fromX = 0.5*x
            fromY = 0.5*y
            toX = 0.5*x
            toY = 0.25*y

        self._swipe(fromX, fromY, toX, toY, steps)

    def swipe_down(self, element=None, steps=10):
        """
        swipe down
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :return: None
        """
        if element:
            x_left, y_up, x_center, y_center, x_right, y_down = self._get_element_size(element)

            fromX = x_center
            fromY = y_center
            toX = x_center
            toY = y_down
        else:
            x, y = self._get_window_size()
            fromX = 0.5*x
            fromY = 0.5*y
            toX = 0.5*x
            toY = 0.75*y

        self._swipe(fromX, fromY, toX, toY, steps)

    def swipe_left(self, element=None, steps=10):
        """
        swipe left
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :return: None
        """
        if element:
            x_left, y_up, x_center, y_center, x_right, y_down = self._get_element_size(element)

            fromX = x_center
            fromY = y_center
            toX = x_left
            toY = y_center
        else:
            x, y = self._get_window_size()
            fromX = 0.5*x
            fromY = 0.5*y
            toX = 0.25*x
            toY = 0.5*y

        self._swipe(fromX, fromY, toX, toY, steps)

    def swipe_right(self, element=None, steps=10):
        """
        swipe right
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :return: None
        """
        if element:
            x_left, y_up, x_center, y_center, x_right, y_down = self._get_element_size(element)

            fromX = x_center
            fromY = y_center
            toX = x_right
            toY = y_center
        else:
            x, y = self._get_window_size()
            fromX = 0.5*x
            fromY = 0.5*y
            toX = 0.75*x
            toY = 0.5*y

        self._swipe(fromX, fromY, toX, toY, steps)

    def _find_element_by_swipe(self, direction, using, value, element=None, steps=10, max_swipe=5):
        times = max_swipe

        for i in range(times):
            try:
                return self.driver.element(using, value)
            except WebDriverException:
                if direction == 'up':
                    self.swipe_up(element=element, steps=steps)
                elif direction == 'down':
                    self.swipe_down(element=element, steps=steps)
                elif direction == 'left':
                    self.swipe_left(element=element, steps=steps)
                elif direction == 'right':
                    self.swipe_right(element=element, steps=steps)

                if i == times - 1:
                    raise WebDriverException

    def find_element_by_swipe_up(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element by swipe up
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        return self._find_element_by_swipe('up', using, value,
                                           element=element, steps=steps, max_swipe=max_swipe)

    def find_element_by_swipe_down(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element by swipe down
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        return self._find_element_by_swipe('down', using, value,
                                           element=element, steps=steps, max_swipe=max_swipe)

    def find_element_by_swipe_left(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element by swipe left
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        return self._find_element_by_swipe('left', using, value,
                                           element=element, steps=steps, max_swipe=max_swipe)

    def find_element_by_swipe_right(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element by swipe right
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        return self._find_element_by_swipe('right', using, value,
                                           element=element, steps=steps, max_swipe=max_swipe)

    def find_element_on_horizontal(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element on horizontal
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        try:
            return self.find_element_by_swipe_left(using, value,
                                                   element=element, steps=steps, max_swipe=max_swipe)
        except WebDriverException:
            pass

        return self.find_element_by_swipe_right(using, value,
                                                element=element, steps=steps, max_swipe=max_swipe)

    def find_element_on_vertical(self, using, value, element=None, steps=10, max_swipe=5):
        """
        find element on vertical
        :param using: The element location strategy.
                      "id","xpath","link text","partial link text","name","tag name","class name","css selector"
        :param value: The value of the location strategy.
        :param element: WebElement of Macaca, if None while swipe window of phone
        :param steps: steps of swipe for Android, The lower the faster
        :param max_swipe: the max times of swipe
        :return: WebElement of Macaca

        Raises:
            WebDriverException.
        """
        try:
            return self.find_element_by_swipe_up(using, value,
                                                 element=element, steps=steps, max_swipe=max_swipe)
        except WebDriverException:
            pass

        return self.find_element_by_swipe_down(using, value,
                                               element=element, steps=steps, max_swipe=max_swipe)

    def _tap(self, x, y):
        self.driver.touch('tap', {'x': x, 'y': y})

    def _click_side_of_element(self, direction, element, rate):
        rect = element.rect

        width = rect['width']
        height = rect['height']

        x_center = rect['x'] + rect['width'] / 2
        y_center = rect['y'] + rect['height'] / 2

        x_left = rect['x']
        y_up = rect['y']
        x_right = rect['x'] + rect['width']
        y_down = rect['y'] + rect['height']

        x = y = 0
        if direction == 'above':
            x = x_center
            y = y_up - rate * height
        elif direction == 'under':
            x = x_center
            y = y_down + rate * height
        elif direction == 'left':
            x = x_left - rate * width
            y = y_center
        elif direction == 'right':
            x = x_right + rate * width
            y = y_center

        self._tap(x, y)

    def click_above_of_element(self, element, rate=1):
        """
        click above the gaven element
        :param element: WebElement of Macaca
        :param rate: rate of the width or height of the element
        :return: None
        """
        self._click_side_of_element('above', element, rate)

    def click_under_of_element(self, element, rate=1):
        """
        click under the gaven element
        :param element: WebElement of Macaca
        :param rate: rate of the width or height of the element
        :return: None
        """
        self._click_side_of_element('under', element, rate)

    def click_left_of_element(self, element, rate=1):
        """
        click the left of the gaven element
        :param element: WebElement of Macaca
        :param rate: rate of the width or height of the element
        :return: None
        """
        self._click_side_of_element('left', element, rate)

    def click_right_of_element(self, element, rate=1):
        """
        click the right of the gaven element
        :param element: WebElement of Macaca
        :param rate: rate of the width or height of the element
        :return: None
        """
        self._click_side_of_element('right', element, rate)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 13 条回复 时间 点赞

真不错,有 github 地址分享吗?

王华林 macaca 怎样左右滑动 中提及了此贴 02月27日 21:32


这里还可以优化,

def _find_element_by_swipe(self, direction, using, value, text,element=None, steps=10, max_swipe=5):
    times = max_swipe

    for i in range(times):

        try:
            ele = self.driver.element(using, value)
            if ele.text == text :
                break
            return ele

        except WebDriverException:
            if direction == 'up':
                self.swipe_up(element=element, steps=steps)
            elif direction == 'down':
                self.swipe_down(element=element, steps=steps)
            elif direction == 'left':
                self.swipe_left(element=element, steps=steps)
            elif direction == 'right':
                self.swipe_right(element=element, steps=steps)

            if i == times - 1:
                raise WebDriverException

加了一个参数 text,加了一个判断,ele 的 text 与 参数 text 一致就可以提前 break 出 for 循环了,不过要是该 ele 没有 text 就难办了😁

_find_element_by_swipe 是个内部方法,是为了给诸如下面的外部方法调用的(目的是返回一个 element,如果找到就直接返回了,不会多滑动的。在没有找到的时候且未超过滑动次数的时候才滑动,超过滑动次数就直接给异常,这里给异常也是符合 macaca 查找 element 逻辑规则的)

而你添加这个参数的目的是什么呢?

def find_element_by_swipe_up(self, using, value, element=None, steps=10, max_swipe=5):
    """
    find element by swipe up
    :param using: The element location strategy.
                  "id","xpath","link text","partial link text","name","tag name","class name","css selector"
    :param value: The value of the location strategy.
    :param element: WebElement of Macaca, if None while swipe window of phone
    :param steps: steps of swipe for Android, The lower the faster
    :param max_swipe: the max times of swipe
    :return: WebElement of Macaca

    Raises:
        WebDriverException.
    """
    return self._find_element_by_swipe('up', using, value,
                                       element=element, steps=steps, max_swipe=max_swipe)
王华林 回复

那个参数是判断是否找到想要的元素。。元素的文本与 text 参数(就是预期该元素的文本),不过看起来我是多此一举了。macaca 还封装了_find_element_by_swipe 这,用 appium 还要自己写,我的思维习惯。。

重来看雨 回复

我理解的分解是这样的,你的目的有两个:
1、需要滑动查找元素
2、判断查找到的元素的 text 是否为某个目标值

所有,按照这个思路就应该分两步,通过诸如这类的方法 find_element_by_swipe_up 获取到 element,然后在用 element.text 去判断。

macaca 本身并没有封装滑动查找元素,这个通用方法是我自己写的。另外,还写了 wait_string、wait_string_use_and、wait_string_use_or、wait_element_by_accessibility_id、click_element_by_accessibility_id,不过这个要后面在看是否分享。

王华林 回复

是的。。之前我只是把划动写好,但划动查找元素以及判断都在 test case 里面写。。现在看到你的展示,我也考虑写在一起,以后会方便很多

重来看雨 回复

是的,封装的目的就是为了降低编写脚本的难度以及增强脚本的可维护性

孟德 基于 录制脚本 的 nodejs 模型 对象拖拽方法 中提及了此贴 04月20日 15:56

你好,self.find_element_on_vertical('id', 'com.platform.jhj:id/home_welfare_more_tv').click() 这个是垂直滑动后 通过 id 的方式查找 com.platform.jhj:id/home_welfare_more_tv 元素吗?

summe 回复

这个方法,先通过你给的方式以及对应的值查找,没有查找到就向上滑动,然后继续找,默认五次,五次后仍没找到就向下滑动按类似方式查找。你看一下调用的方法就能看出

@hualin 这个能实现 iOS 的左滑操作效果吗?

webview 可以用么?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册