Python侧开28期-偕行-学习笔记-appium高级控件交互-ActionChains

高级控件交互(操作)–ActionChains

  • 说明:
    • 1、appium中的ActionChains用到就是selenium中的ActionChains,导包的时候导入的是selenium的包;
    • 2、而具体用法却和selenium有点区别:
      • selenium中:直接初始化ActionChains 后就可以直接调用相关方法;
      • appium中:初始化ActionChains之后,需要使用w3c_actions属性去创建输入源,之后执行w3c_actions下的方法;
    • 3、actions-w3c官网

(1)ActionChains构造方法源码—初始化actions:

  • actions = ActionChains(driver)
class ActionChains:
    def __init__(self, driver: WebDriver, duration: int = 250, devices: list[AnyDevice] | None = None) -> None:
        """Creates a new ActionChains.

        :Args:
         - driver: The WebDriver instance which performs user actions.
         - duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput
        """
        self._driver = driver
        mouse = None
        keyboard = None
        wheel = None
        if devices is not None and isinstance(devices, list):
            for device in devices:
                if isinstance(device, PointerInput):
                    mouse = device
                if isinstance(device, KeyInput):
                    keyboard = device
                if isinstance(device, WheelInput):
                    wheel = device
        self.w3c_actions = ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration)
  • self.w3c_actions 属性:就是用来定义appium中的事件;
  • ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration):用来定义输入源;
    • mouse:鼠标输入源
    • keyboard:键盘输入源
    • wheel:鼠标滚轮输入源
    • duration:

(2)ActionBuilder源码—获取动作构造器

ActionBuilder的构造方法:
actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, “touch”))

class ActionBuilder:
    def __init__(
        self,
        driver,
        mouse: Optional[PointerInput] = None,
        wheel: Optional[WheelInput] = None,
        keyboard: Optional[KeyInput] = None,
        duration: int = 250,
    ) -> None:
        mouse = mouse or PointerInput(interaction.POINTER_MOUSE, "mouse")
        keyboard = keyboard or KeyInput(interaction.KEY)
        wheel = wheel or WheelInput(interaction.WHEEL)
        self.devices = [mouse, keyboard, wheel]
        self._key_action = KeyActions(keyboard)
        self._pointer_action = PointerActions(mouse, duration=duration)
        self._wheel_action = WheelActions(wheel)
        self.driver = driver
  • PointerInput:鼠标指针输入选项
  • WheelInput:鼠标滚轮输入选项
  • KeyInput:键盘输入选项

ActionBuilder中的属性和方法:

    def get_device_with(self, name: str) -> Optional[Union["WheelInput", "PointerInput", "KeyInput"]]:
        return next(filter(lambda x: x == name, self.devices), None)

    @property
    def pointer_inputs(self) -> List[PointerInput]:
        return [device for device in self.devices if device.type == interaction.POINTER]

    @property
    def key_inputs(self) -> List[KeyInput]:
        return [device for device in self.devices if device.type == interaction.KEY]

    @property
    def key_action(self) -> KeyActions:
        return self._key_action

    @property
    def pointer_action(self) -> PointerActions:
        return self._pointer_action

    @property
    def wheel_action(self) -> WheelActions:
        return self._wheel_action

    def add_key_input(self, name: str) -> KeyInput:
        new_input = KeyInput(name)
        self._add_input(new_input)
        return new_input

    def add_pointer_input(self, kind: str, name: str) -> PointerInput:
        new_input = PointerInput(kind, name)
        self._add_input(new_input)
        return new_input

    def add_wheel_input(self, name: str) -> WheelInput:
        new_input = WheelInput(name)
        self._add_input(new_input)
        return new_input

    def perform(self) -> None:
        enc = {"actions": []}
        for device in self.devices:
            encoded = device.encode()
            if encoded["actions"]:
                enc["actions"].append(encoded)
                device.actions = []
        self.driver.execute(Command.W3C_ACTIONS, enc)

    def clear_actions(self) -> None:
        """Clears actions that are already stored on the remote end."""
        self.driver.execute(Command.W3C_CLEAR_ACTIONS)

    def _add_input(self, new_input: Union[KeyInput, PointerInput, WheelInput]) -> None:
        self.devices.append(new_input)

(3)PointerInput构造方法源码—创建输入源

  • mouse=PointerInput(interaction.POINTER_TOUCH, "touch")
class PointerInput(InputDevice):
    DEFAULT_MOVE_DURATION = 250

    def __init__(self, kind, name):
        super().__init__()
        if kind not in POINTER_KINDS:
            raise InvalidArgumentException(f"Invalid PointerInput kind '{kind}'")
        self.type = POINTER
        self.kind = kind
        self.name = name
  • kind:鼠标指针输入类型
  • name:鼠标指针输入名字

(4)interaction模块源码—输入源类型

from typing import Dict
from typing import Union

KEY = "key"
POINTER = "pointer"
NONE = "none"
WHEEL = "wheel"
SOURCE_TYPES = {KEY, POINTER, NONE}

POINTER_MOUSE = "mouse"
POINTER_TOUCH = "touch"
POINTER_PEN = "pen"

POINTER_KINDS = {POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN}

class Interaction:
    PAUSE = "pause"

    def __init__(self, source: str) -> None:
        self.source = source

class Pause(Interaction):
    def __init__(self, source, duration: float = 0) -> None:
        super().__init__(source)
        self.duration = duration

    def encode(self) -> Dict[str, Union[str, int]]:
        return {"type": self.PAUSE, "duration": int(self.duration * 1000)}

(5)actions.w3c_actions.pointer_action–指针动作

  • pointer_down():鼠标左键按下—常用
  • pointer_up():鼠标左前抬起—常用
  • move_to():
  • move_by():
  • move_to_location():移动到位置—常用
  • click():鼠标左键点击—常用
  • context_click():鼠标右键点击—常用
  • click_and_hold():鼠标左键按住—常用
  • release():鼠标释放—常用
  • double_click():双击
  • pause():暂停
class PointerActions(Interaction):
    def __init__(self, source: Optional[PointerInput] = None, duration: int = 250):
        """
        Args:
        - source: PointerInput instance
        - duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in source
        """
        if not source:
            source = PointerInput(interaction.POINTER_MOUSE, "mouse")
        self.source = source
        self._duration = duration
        super().__init__(source)

    def pointer_down(
        self,
        button=MouseButton.LEFT,
        width=None,
        height=None,
        pressure=None,
        tangential_pressure=None,
        tilt_x=None,
        tilt_y=None,
        twist=None,
        altitude_angle=None,
        azimuth_angle=None,
    ):
        self._button_action(
            "create_pointer_down",
            button=button,
            width=width,
            height=height,
            pressure=pressure,
            tangential_pressure=tangential_pressure,
            tilt_x=tilt_x,
            tilt_y=tilt_y,
            twist=twist,
            altitude_angle=altitude_angle,
            azimuth_angle=azimuth_angle,
        )
        return self

    def pointer_up(self, button=MouseButton.LEFT):
        self._button_action("create_pointer_up", button=button)
        return self

    def move_to(
        self,
        element,
        x=0,
        y=0,
        width=None,
        height=None,
        pressure=None,
        tangential_pressure=None,
        tilt_x=None,
        tilt_y=None,
        twist=None,
        altitude_angle=None,
        azimuth_angle=None,
    ):
        if not isinstance(element, WebElement):
            raise AttributeError("move_to requires a WebElement")

        self.source.create_pointer_move(
            origin=element,
            duration=self._duration,
            x=int(x),
            y=int(y),
            width=width,
            height=height,
            pressure=pressure,
            tangential_pressure=tangential_pressure,
            tilt_x=tilt_x,
            tilt_y=tilt_y,
            twist=twist,
            altitude_angle=altitude_angle,
            azimuth_angle=azimuth_angle,
        )
        return self

    def move_by(
        self,
        x,
        y,
        width=None,
        height=None,
        pressure=None,
        tangential_pressure=None,
        tilt_x=None,
        tilt_y=None,
        twist=None,
        altitude_angle=None,
        azimuth_angle=None,
    ):
        self.source.create_pointer_move(
            origin=interaction.POINTER,
            duration=self._duration,
            x=int(x),
            y=int(y),
            width=width,
            height=height,
            pressure=pressure,
            tangential_pressure=tangential_pressure,
            tilt_x=tilt_x,
            tilt_y=tilt_y,
            twist=twist,
            altitude_angle=altitude_angle,
            azimuth_angle=azimuth_angle,
        )
        return self

    def move_to_location(
        self,
        x,
        y,
        width=None,
        height=None,
        pressure=None,
        tangential_pressure=None,
        tilt_x=None,
        tilt_y=None,
        twist=None,
        altitude_angle=None,
        azimuth_angle=None,
    ):
        self.source.create_pointer_move(
            origin="viewport",
            duration=self._duration,
            x=int(x),
            y=int(y),
            width=width,
            height=height,
            pressure=pressure,
            tangential_pressure=tangential_pressure,
            tilt_x=tilt_x,
            tilt_y=tilt_y,
            twist=twist,
            altitude_angle=altitude_angle,
            azimuth_angle=azimuth_angle,
        )
        return self

    def click(self, element: Optional[WebElement] = None, button=MouseButton.LEFT):
        if element:
            self.move_to(element)
        self.pointer_down(button)
        self.pointer_up(button)
        return self

    def context_click(self, element: Optional[WebElement] = None):
        return self.click(element=element, button=MouseButton.RIGHT)

    def click_and_hold(self, element: Optional[WebElement] = None, button=MouseButton.LEFT):
        if element:
            self.move_to(element)
        self.pointer_down(button=button)
        return self

    def release(self, button=MouseButton.LEFT):
        self.pointer_up(button=button)
        return self

    def double_click(self, element: Optional[WebElement] = None):
        if element:
            self.move_to(element)
        self.pointer_down(MouseButton.LEFT)
        self.pointer_up(MouseButton.LEFT)
        self.pointer_down(MouseButton.LEFT)
        self.pointer_up(MouseButton.LEFT)
        return self

    def pause(self, duration: float = 0):
        self.source.create_pause(duration)
        return self

    def _button_action(self, action, **kwargs):
        meth = getattr(self.source, action)
        meth(**kwargs)
        return self

(6)使用步骤

  • 1、定义 ActionChains 实例:ActionChains(driver)
  • 2、定义输入源:actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
    • ActionChains里有个属性w3c_actions 是ActionBuilder类型的, 使用的就是w3c协议,可以定义鼠标指针源,键盘源,滚轮源事件;
  • 3、调用动作:移动、按下、滑动、抬起等等;
    • actions.w3c_actions.pointer_action.move_to_location(115, 183)
    • actions.w3c_actions.pointer_action.pointer_down()
    • actions.w3c_actions.pointer_action.move_to_location(362, 179)
  • 4、释放指针: actions.w3c_actions.pointer_action.release()
  • 5、执行动作:actions.perform()

(7)案例-使用ActionChains处理手势滑动解锁

  • touchaction.apk下载地址如下-百度网盘:
  • 说明:手势解锁一般都是封装好的api,通过常规定位无法定位到,只能通过坐标进行操作;
import time
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver import ActionChains
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput

caps = {
    "platformName": "Android",
    "appium:platformVersion": "6.0.1",
    "appium:deviceName": "127.0.0.1:7555",
    "appium:appPackage": "cn.kmob.screenfingermovelock",
    "appium:appActivity": "com.samsung.ui.FlashActivity",
    "appium:automationName": "UiAutomator2",
    "appium:unicodeKeyboard": "true",
    "appium:restKeyboard": "true",
    "appium:noReset": "true"
}
surver_url = "http://127.0.0.1:4723"
driver = webdriver.Remote(surver_url,options=UiAutomator2Options().load_capabilities(caps=caps))
driver.implicitly_wait(10)
# 点击设置手势-进入手势密码设置界面
driver.find_element(AppiumBy.ID,"cn.kmob.screenfingermovelock:id/setPatternLayout").click()

# 1、创建动作实例
action = ActionChains(driver)
# 2、输入动作源--指针输入--触摸类型
action.w3c_actions = ActionBuilder(driver=driver,mouse=PointerInput(kind=interaction.POINTER_TOUCH,name="touch"))
# 3、添加动作--鼠标移动到第一个点之后按下
action.w3c_actions.pointer_action.move_to_location(x=120,y=175)
action.w3c_actions.pointer_action.pointer_down()
time.sleep(1)
# 指针移动到其他点
# 第二个点
action.w3c_actions.pointer_action.move_to_location(x=360,y=175)
time.sleep(1)
# 第三个点
action.w3c_actions.pointer_action.move_to_location(x=600,y=175)
time.sleep(1)
# 第四个点
action.w3c_actions.pointer_action.move_to_location(x=600,y=415)
time.sleep(0.1)
# 第五个点
action.w3c_actions.pointer_action.move_to_location(x=600,y=650)
time.sleep(0.1)

# 4、释放指针
action.w3c_actions.pointer_action.release()
# 5、执行动作
action.perform()

image

长按固定坐标点xy

def touch_long_press_xy(self, x, y):
    """
    长按固定坐标点xy
    :param x: x坐标
    :param y: y坐标
    """
    try:
        actions = ActionChains(self.driver)
        # 可以定义鼠标指针源,键盘源,滚轮源事件
        actions.w3c_actions = ActionBuilder(self.driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
        # 移动到坐标点
        actions.w3c_actions.pointer_action.move_to_location(x, y)
        # 按下
        actions.w3c_actions.pointer_action.pointer_down()
        sleep(1)
        #print(actions.w3c_actions.pointer_action.pointer_down())
        #停顿3s
        actions.w3c_actions.pointer_action.pause(3)
        # 释放
        actions.w3c_actions.pointer_action.pointer_up()
        # 执行
        actions.perform()
    except Exception as e:
        logger.error(f"Error: {e}")

目前有两个问题:
(版本:Appium-Python-Client 2.11.0,selenium 4.15.2)
1、在小米设备上执行,调用到这个函数会报: [ERROR]: Error: Message: An unknown server-side error occurred while processing the command. Original error: Unable to perform W3C actions. Check the logcat output for possible error reports and make sure your input actions chain is valid.
但不影响最终结果,最终passed


2、在vivo pad上执行看服务器返回信息,有按顺序执行,状态返回200,但观察执行动作,未按照预期执行长按预设坐标的操作,不知为什么?
求大神指教!!!