diff --git a/README.md b/README.md index 2c1d9343..054f13e6 100644 --- a/README.md +++ b/README.md @@ -124,10 +124,12 @@ For example, some changes in the Selenium binding could break the Appium client. - `MultiAction` and `TouchAction` are deprecated. Please use W3C WebDriver actions or `mobile:` extensions - `launch_app`, `close_app`, and `reset` are deprecated. Please read [issues#15807](https://github.com/appium/appium/issues/15807) for more details -#### MultiAction/TouchAction to W3C actions +#### MultiAction/TouchAction and ActionHelpers Shortcuts to W3C Actions + +The interaction shortcut methods on `ActionHelpers` (`scroll`, `drag_and_drop`, `tap`, `swipe`, and `flick`) are deprecated because gesture behavior varies across drivers and pointer types (for example, Android UiAutomator2 handles `POINTER_TOUCH` and `POINTER_MOUSE` inputs differently in the underlying UIAutomator framework). Build W3C WebDriver actions directly with `ActionChains` / `ActionBuilder` or use driver-specific `mobile:` gesture extensions instead. Some elements can be handled with `touch` pointer action instead of the default `mouse` pointer action in the Selenium Python client. -For example, the below action builder is to replace the default one with the `touch` pointer action. +For example, the below action builder is to replace the default one with the `touch` pointer action: ```python from selenium.webdriver import ActionChains @@ -136,7 +138,7 @@ from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.pointer_input import PointerInput actions = ActionChains(driver) -# override as 'touch' pointer action +# override as 'touch' pointer action (or use interaction.POINTER_MOUSE for mouse input) actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, 'touch')) actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) actions.w3c_actions.pointer_action.pointer_down() @@ -146,6 +148,19 @@ actions.w3c_actions.pointer_action.release() actions.perform() ``` +Or use native `mobile:` gesture commands via `driver.execute_script`: + +```python +# iOS (XCUITest) scroll example +driver.execute_script('mobile: scroll', {'direction': 'down'}) + +# Android (UiAutomator2) scrollGesture example +driver.execute_script( + 'mobile: scrollGesture', + {'left': 100, 'top': 100, 'width': 200, 'height': 400, 'direction': 'down', 'percent': 1.0}, +) +``` + - [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py) - https://www.selenium.dev/documentation/webdriver/actions_api/ diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index be32ca9f..669b4a77 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -19,7 +19,7 @@ from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.mouse_button import MouseButton from selenium.webdriver.common.actions.pointer_input import PointerInput -from typing_extensions import Self +from typing_extensions import Self, deprecated from appium.webdriver.webelement import WebElement @@ -29,9 +29,14 @@ class ActionHelpers: + @deprecated('Please use W3C actions (ActionChains/ActionBuilder) or mobile: gesture extensions instead.') def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: int | None = None) -> Self: """Scrolls from one element to another + .. deprecated:: + Please use W3C actions (`ActionChains` / `ActionBuilder`) or driver-specific + `mobile:` gesture extensions instead. + Args: origin_el: the element from which to begin scrolling (center of element) destination_el: the element to scroll to (center of element) @@ -63,9 +68,14 @@ def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: in actions.perform() return self + @deprecated('Please use W3C actions (ActionChains/ActionBuilder) or mobile: gesture extensions instead.') def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement, pause: float | None = None) -> Self: """Drag the origin element to the destination element + .. deprecated:: + Please use W3C actions (`ActionChains` / `ActionBuilder`) or driver-specific + `mobile:` gesture extensions instead. + Args: origin_el: the element to drag destination_el: the element to drag to @@ -84,10 +94,15 @@ def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement, pause actions.perform() return self + @deprecated('Please use W3C actions (ActionChains/ActionBuilder) or mobile: gesture extensions instead.') def tap(self, positions: list[tuple[int, int]], duration: int | None = None) -> Self: """Taps on an particular place with up to five fingers, holding for a certain time + .. deprecated:: + Please use W3C actions (`ActionChains` / `ActionBuilder`) or driver-specific + `mobile:` gesture extensions instead. + Args: positions: an array of tuples representing the x/y coordinates of the fingers to tap. Length can be up to five. @@ -132,9 +147,14 @@ def tap(self, positions: list[tuple[int, int]], duration: int | None = None) -> actions.perform() return self + @deprecated('Please use W3C actions (ActionChains/ActionBuilder) or mobile: gesture extensions instead.') def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> Self: """Swipe from one point to another point, for an optional duration. + .. deprecated:: + Please use W3C actions (`ActionChains` / `ActionBuilder`) or driver-specific + `mobile:` gesture extensions instead. + Args: start_x: x-coordinate at which to start start_y: y-coordinate at which to start @@ -161,9 +181,14 @@ def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: in actions.perform() return self + @deprecated('Please use W3C actions (ActionChains/ActionBuilder) or mobile: gesture extensions instead.') def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self: """Flick from one point to another point. + .. deprecated:: + Please use W3C actions (`ActionChains` / `ActionBuilder`) or driver-specific + `mobile:` gesture extensions instead. + Args: start_x: x-coordinate at which to start start_y: y-coordinate at which to start diff --git a/test/unit/webdriver/device/action_helpers_test.py b/test/unit/webdriver/device/action_helpers_test.py new file mode 100644 index 00000000..d700ea58 --- /dev/null +++ b/test/unit/webdriver/device/action_helpers_test.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import httpretty +import pytest + +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body + + +class TestActionHelpers: + @httpretty.activate + def test_scroll_emits_deprecation_warning(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/actions')) + el1 = MobileWebElement(driver, 'el1') + el2 = MobileWebElement(driver, 'el2') + + with pytest.deprecated_call(): + driver.scroll(el1, el2) + + body = get_httpretty_request_body(httpretty.last_request()) + assert body['actions'][0]['type'] == 'pointer' + assert body['actions'][0]['parameters']['pointerType'] == 'touch' + + @httpretty.activate + def test_drag_and_drop_emits_deprecation_warning(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/actions')) + el1 = MobileWebElement(driver, 'el1') + el2 = MobileWebElement(driver, 'el2') + + with pytest.deprecated_call(): + driver.drag_and_drop(el1, el2) + + body = get_httpretty_request_body(httpretty.last_request()) + assert body['actions'][0]['type'] == 'pointer' + + @httpretty.activate + def test_tap_emits_deprecation_warning(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/actions')) + + with pytest.deprecated_call(): + driver.tap([(100, 200)], 500) + + body = get_httpretty_request_body(httpretty.last_request()) + assert body['actions'][0]['type'] == 'pointer' + + @httpretty.activate + def test_swipe_emits_deprecation_warning(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/actions')) + + with pytest.deprecated_call(): + driver.swipe(100, 100, 100, 400, 300) + + body = get_httpretty_request_body(httpretty.last_request()) + assert body['actions'][0]['type'] == 'pointer' + + @httpretty.activate + def test_flick_emits_deprecation_warning(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/actions')) + + with pytest.deprecated_call(): + driver.flick(100, 100, 100, 400) + + body = get_httpretty_request_body(httpretty.last_request()) + assert body['actions'][0]['type'] == 'pointer'