Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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/

Expand Down
27 changes: 26 additions & 1 deletion appium/webdriver/extensions/action_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
81 changes: 81 additions & 0 deletions test/unit/webdriver/device/action_helpers_test.py
Original file line number Diff line number Diff line change
@@ -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'