blob: 4471038f061767c508bf53b33470c58693786ec5 [file] [log] [blame]
# Copyright 2023 Google Inc.
#
# 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.
"""This module is corresponding to the "androidx.test.uiautomator.UiObject2".
https://developer.android.com/reference/androidx/test/uiautomator/UiObject2
"""
from __future__ import annotations
import copy
from typing import Literal, Mapping, Optional, Sequence, Union
import warnings
from mobly.controllers.android_device_lib import snippet_client_v2
from snippet_uiautomator import byselector
from snippet_uiautomator import constants
from snippet_uiautomator import errors
from snippet_uiautomator import utils
# Since uiobject2 access the private RPC client, disable the warning.
# pylint: disable=protected-access
class _Click:
"""Performs a click action on a specific UiObject2."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
) -> None:
self._ui = ui
self._selector = selector
self._rpc_timeout_ms = utils.covert_to_millisecond(
constants.DEFAULT_SNIPPET_RPC_TIMEOUT
)
def __call__(
self,
duration: Optional[utils.TimeUnit] = None,
timeout: Optional[utils.TimeUnit] = None,
x: Optional[int] = None,
y: Optional[int] = None,
) -> bool:
"""Clicks on this object.
Args:
duration: The time in milliseconds to click and hold.
timeout: (Deprecated) The time in milliseconds to click and hold.
x: The X coordinate of the point to click (within the visible bounds).
y: The Y coordinate of the point to click (within the visible bounds).
Returns:
True if operation succeeds, False otherwise.
Raises:
errors.ApiError: When given incorrect arguments to this method.
"""
if timeout is not None:
warnings.warn(
"The 'timeout' argument is deprecated, use 'duration' instead.",
DeprecationWarning,
stacklevel=2,
)
duration = timeout # Maintain backward compatibility
duration_ms = (
None if duration is None else utils.covert_to_millisecond(duration)
)
if x is not None and y is not None:
return self._ui.clickObjPoint(self._selector.to_dict(), x, y, duration_ms)
elif x is None and y is None:
return self._ui.clickObj(self._selector.to_dict(), duration_ms)
else:
raise errors.ApiError('Must provide both x and y to click on point')
def bottomright(self) -> bool:
"""Clicks the lower right corner of this object."""
bounds = self._ui.getVisibleBounds(self._selector.to_dict())
if bounds is None:
return False
return self._ui.click(bounds['right'], bounds['bottom'])
def topleft(self) -> bool:
"""Clicks the upper left corner of this object."""
bounds = self._ui.getVisibleBounds(self._selector.to_dict())
if bounds is None:
return False
return self._ui.click(bounds['left'], bounds['top'])
def wait(
self, timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME
) -> bool:
"""Clicks on this object and waits for window transitions.
Args:
timeout: The time in milliseconds to wait for window transitions after
clicked.
Returns:
True if a window update occurred after clicked, False otherwise.
Raises:
errors.ApiError: When the timeout is longer than the default RPC timeout.
"""
timeout_ms = utils.covert_to_millisecond(timeout)
if timeout_ms >= self._rpc_timeout_ms:
raise errors.ApiError(constants.ERROR_MSG_FOR_LONG_TIMEOUT)
return self._ui.clickObjAndWait(self._selector.to_dict(), timeout_ms)
class _Drag:
"""Performs a drag action on a specific UiObject2."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
def __call__(
self,
x: Optional[int] = None,
y: Optional[int] = None,
speed: Optional[int] = None,
**kwargs,
) -> bool:
return self.to(x, y, speed, **kwargs)
def to(
self,
x: Optional[int] = None,
y: Optional[int] = None,
speed: Optional[int] = None,
**kwargs,
) -> bool:
"""Drags this object to the specified location.
Args:
x: The X coordinate of the destination.
y: The Y coordinate of the destination.
speed: The speed at which to perform this gesture in pixels per second.
**kwargs: The search criteria for matching the destination object.
Returns:
True if operation succeeds, False otherwise.
Raises:
errors.ApiError: When given incorrect arguments to this method.
"""
if x is None and y is None and kwargs:
return self._ui.dragObjToObj(
self._selector.to_dict(),
byselector.BySelector(**kwargs).to_dict(),
speed,
)
elif x is not None and y is not None and not kwargs:
return self._ui.dragObj(self._selector.to_dict(), x, y, speed)
else:
raise errors.ApiError(
'Drag to object and drag to coordinates cannot be mixed', self._device
)
class _Gesture:
"""Performs swipe or fling gesture in a specific direction on a specific UiObject2."""
class _To:
"""Performs swipe or fling gesture from this object in a specific direction."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
action: Literal['swipe', 'fling'],
direction: str,
margin: Optional[int],
percent: Optional[int],
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._action = action
self._direction = direction
self._margin = margin
self._percent = percent
def __call__(self, percent: int = 0, speed: Optional[int] = None) -> bool:
"""Performs swipe or fling gesture on this object with specified direction.
Args:
percent: The length of the swipe or the distance to scroll as a
percentage of this object's size. This value must between 0 and 100.
speed: The speed at which to perform this gesture in pixels per second.
Returns:
True if operation succeeds, False otherwise.
"""
if self._action == 'fling':
if percent != 0:
raise errors.ApiError(
'fling gesture does not support changing the percent',
self._device,
)
return self._ui.fling(
self._selector.to_dict(),
self._direction,
speed,
self._margin,
self._percent,
)
elif self._action == 'swipe':
if not (0 <= percent <= 100):
raise errors.ApiError(
'swipe gesture requires percent to be between 0 and 100',
self._device,
)
return self._ui.swipeObj(
self._selector.to_dict(),
self._direction,
percent,
speed,
self._margin,
self._percent,
)
else:
raise errors.ApiError(
f'Unknown gesture action: {self._action}', self._device
)
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
action: Literal['swipe', 'fling'],
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._action = action
self._margin = None
self._percent = None
def __call__(
self, margin: Optional[int] = None, percent: Optional[int] = None
) -> _Gesture:
"""Sets the margins used for scroll gesture."""
if margin is not None and percent is not None:
raise errors.ApiError(
'Pixel-based and percentage-based margin cannot be mixed',
self._device,
)
self._margin = margin
self._percent = percent
return self
@property
def down(self) -> _Gesture._To:
"""Performs a gesture on this object with direction DOWN."""
return self._To(
self._ui,
self._selector,
self._action,
'DOWN',
self._margin,
self._percent,
)
@property
def left(self) -> _Gesture._To:
"""Performs a gesture on this object with direction LEFT."""
return self._To(
self._ui,
self._selector,
self._action,
'LEFT',
self._margin,
self._percent,
)
@property
def right(self) -> _Gesture._To:
"""Performs a gesture on this object with direction RIGHT."""
return self._To(
self._ui,
self._selector,
self._action,
'RIGHT',
self._margin,
self._percent,
)
@property
def up(self) -> _Gesture._To:
"""Performs a gesture on this object with direction UP."""
return self._To(
self._ui,
self._selector,
self._action,
'UP',
self._margin,
self._percent,
)
class _Pinch:
"""Performs a pinch gesture on a specific UiObject2."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
) -> None:
self._ui = ui
self._selector = selector
def close(self, percent: int, speed: Optional[int] = None) -> bool:
"""Performs a pinch close gesture on this object.
Args:
percent: The size of the pinch as a percentage of this object's size.
speed: The speed at which to perform this gesture in pixels per second.
Returns:
True if operation succeeds, False otherwise.
"""
selector_dict = self._selector.to_dict()
return self._ui.pinchClose(selector_dict, percent, speed)
def open(self, percent: int, speed: Optional[int] = None) -> bool:
"""Performs a pinch open gesture on this object.
Args:
percent: The size of the pinch as a percentage of this object's size.
speed: The speed at which to perform this gesture in pixels per second.
Returns:
True if operation succeeds, False otherwise.
"""
selector_dict = self._selector.to_dict()
return self._ui.pinchOpen(selector_dict, percent, speed)
class _Scroll:
"""Performs a scroll action on a specific UiObject2."""
class _To:
"""Scrolls from this object to specific position or object."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
direction: str,
margin: Optional[int],
percent: Optional[int],
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._direction = direction
self._margin = margin
self._percent = percent
def __call__(
self,
percent: Optional[int] = None,
speed: Optional[int] = None,
target: Optional[UiObject2] = None,
**kwargs,
) -> bool:
"""Scrolls to specific position or until target object is visible.
Args:
percent: The length of the swipe or the distance to scroll as a
percentage of this object's size. This value must between 0 and 100.
speed: The speed at which to perform this gesture in pixels per second.
target: The target object to scroll to.
**kwargs: The search criteria for matching objects.
Returns:
True if operation succeed, False otherwise.
Raises:
errors.ApiError: When given incorrect arguments to this method.
"""
if percent is not None and target is None and not kwargs:
return self._ui.scroll(
self._selector.to_dict(),
self._direction,
percent,
speed,
self._margin,
self._percent,
)
if percent is None and speed is None:
if target is None and kwargs:
return self._ui.scrollUntil(
self._selector.to_dict(),
kwargs,
self._direction,
self._margin,
self._percent,
)
elif target is None and not kwargs:
return self._ui.scrollUntilFinished(
self._selector.to_dict(),
self._direction,
self._margin,
self._percent,
)
elif target is not None and not kwargs:
return self._ui.scrollUntil(
self._selector.to_dict(),
target._selector.to_dict(),
self._direction,
self._margin,
self._percent,
)
raise errors.ApiError(
'Scroll by percentage and scroll by condition cannot be mixed',
self._device,
)
def click(self, **kwargs) -> bool:
"""Scrolls until the target object is visible, then clicks."""
if not kwargs:
raise errors.ApiError(
'Target to scroll to is not defined', self._device
)
if self(**kwargs):
return self._ui.clickObj(kwargs)
return False
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._margin = None
self._percent = None
def __call__(
self, margin: Optional[int] = None, percent: Optional[int] = None
) -> _Scroll:
"""Sets the margins used for scroll gesture."""
if margin is not None and percent is not None:
raise errors.ApiError(
'Pixel-based and percentage-based margin cannot be mixed',
self._device,
)
self._margin = margin
self._percent = percent
return self
@property
def down(self) -> _Scroll._To:
"""Performs a scroll gesture on this object with direction DOWN."""
return self._To(
self._ui, self._selector, 'DOWN', self._margin, self._percent
)
@property
def left(self) -> _Scroll._To:
"""Performs a scroll gesture on this object with direction LEFT."""
return self._To(
self._ui, self._selector, 'LEFT', self._margin, self._percent
)
@property
def right(self) -> _Scroll._To:
"""Performs a scroll gesture on this object with direction RIGHT."""
return self._To(
self._ui, self._selector, 'RIGHT', self._margin, self._percent
)
@property
def up(self) -> _Scroll._To:
"""Performs a scroll gesture on this object with direction UP."""
return self._To(self._ui, self._selector, 'UP', self._margin, self._percent)
class _Wait:
"""Waits for a specific UiObject2 to appear or disappear."""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
raise_error: bool = False,
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._raise_error = raise_error
self._rpc_timeout_ms = utils.covert_to_millisecond(
constants.DEFAULT_SNIPPET_RPC_TIMEOUT
)
def click(
self, timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME
) -> bool:
"""Waits for this object to appear, then clicks.
Args:
timeout: The time in milliseconds to wait to appear.
Returns:
True if the object exists and click successfully, False otherwise.
Raises:
errors.ApiError: When the timeout is longer than the default RPC timeout.
"""
timeout_ms = utils.covert_to_millisecond(timeout)
if timeout_ms >= self._rpc_timeout_ms:
raise errors.ApiError(constants.ERROR_MSG_FOR_LONG_TIMEOUT)
if self._ui.waitForExists(self._selector.to_dict(), timeout_ms):
return self._ui.clickObj(self._selector.to_dict())
return False
def exists(
self,
timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME,
raise_error: bool = False,
) -> bool:
"""Waits for this object to appear.
Args:
timeout: The time in milliseconds to wait to appear.
raise_error: Set True to raise an error when the object is not found,
False to return a False value.
Returns:
True if this object exists, False otherwise.
Raises:
errors.ApiError: When the timeout is longer than the default RPC timeout.
errors.UiObjectSearchError: When the object is not found and raise_error
is True.
"""
timeout_ms = utils.covert_to_millisecond(timeout)
if timeout_ms >= self._rpc_timeout_ms:
raise errors.ApiError(constants.ERROR_MSG_FOR_LONG_TIMEOUT)
is_exists = self._ui.waitForExists(self._selector.to_dict(), timeout_ms)
if is_exists:
return True
if self._raise_error or raise_error:
raise errors.UiObjectSearchError(
f'Not found Selector{self._selector.to_dict()} over {timeout_ms} ms',
self._device,
)
return False
def assert_exists(
self,
error_msg: str,
timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME,
) -> None:
"""Asserts that this object exists before the timeout."""
try:
self.exists(timeout, raise_error=True)
except errors.UiObjectSearchError as e:
raise errors.UiObjectSearchError(error_msg, self._device) from e
def gone(
self,
timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME,
raise_error: bool = False,
) -> bool:
"""Waits for this object to disappear.
Args:
timeout: The time in milliseconds to wait to disappear.
raise_error: Set True to raise an error when the object is still found,
False to return a False value.
Returns:
True if this object was not found, False otherwise.
Raises:
errors.ApiError: When the timeout is longer than the default RPC timeout.
errors.UiObjectSearchError: When the object is still found and raise_error
is True.
"""
timeout_ms = utils.covert_to_millisecond(timeout)
if timeout_ms >= self._rpc_timeout_ms:
raise errors.ApiError(constants.ERROR_MSG_FOR_LONG_TIMEOUT)
is_gone = self._ui.waitUntilGone(self._selector.to_dict(), timeout_ms)
if is_gone:
return True
if self._raise_error or raise_error:
raise errors.UiObjectSearchError(
f'Still found Selector{self._selector.to_dict()} over'
f' {timeout_ms} ms',
self._device,
)
return False
def assert_gone(
self,
error_msg: str,
timeout: utils.TimeUnit = constants.DEFAULT_UI_WAIT_TIME,
) -> None:
"""Asserts that this object goes before the timeout."""
try:
self.gone(timeout, raise_error=True)
except errors.UiObjectSearchError as e:
raise errors.UiObjectSearchError(error_msg, self._device) from e
class UiObject2:
"""Represents an UiObject2.
https://developer.android.com/reference/androidx/test/uiautomator/UiObject2
"""
def __init__(
self,
ui: snippet_client_v2.SnippetClientV2,
selector: byselector.BySelector,
raise_error: bool = False,
) -> None:
self._ui = ui
self._device = self._ui._device
self._selector = selector
self._raise_error = raise_error
def _create_instance(self, tag: str, **kwargs) -> UiObject2:
"""Creates a new instance of this object with the given tag."""
selector = copy.deepcopy(self._selector)
selector.append(tag, **kwargs)
return UiObject2(self._ui, selector, self._raise_error)
@property
def parent(self) -> UiObject2:
"""Finds this object's parent, or null if it has no parent."""
return self._create_instance('parent')
def ancestor(self, **kwargs) -> UiObject2:
"""Finds this object's ancestor, or null if it has no matched ancestor."""
return self._create_instance('ancestor', **kwargs)
def child(self, **kwargs) -> UiObject2:
"""Finds the child object directly under this object."""
return self._create_instance('child', **kwargs)
def sibling(self, **kwargs) -> UiObject2:
"""Finds the sibling object which has the same parent with this object."""
return self._create_instance('sibling', **kwargs)
def bottom(self, **kwargs) -> UiObject2:
"""Finds the closest object that is below this object."""
return self._create_instance('bottom', **kwargs)
def left(self, **kwargs) -> UiObject2:
"""Finds the closest object that is to the left of this object."""
return self._create_instance('left', **kwargs)
def right(self, **kwargs) -> UiObject2:
"""Finds the closest object that is to the right of this object."""
return self._create_instance('right', **kwargs)
def top(self, **kwargs) -> UiObject2:
"""Finds the closest object that is above this object."""
return self._create_instance('top', **kwargs)
def clear_text(self) -> bool:
"""Clears the text content if this object is an editable field."""
return self._ui.clear(self._selector.to_dict())
def find(self, **kwargs) -> Sequence[byselector.SelectorType]:
"""Finds all objects under this object to match the selector criteria."""
return self._ui.findChildObjects(
self._selector.to_dict(), byselector.BySelector(**kwargs).to_dict()
)
def has(self, **kwargs) -> bool:
"""Returns if there is a match for the given criteria under this object."""
return self._ui.hasChildObject(
self._selector.to_dict(), byselector.BySelector(**kwargs).to_dict()
)
def long_click(self) -> bool:
"""Performs a long click on this object."""
return self._ui.longClick(self._selector.to_dict())
def set_text(self, text: str) -> bool:
"""Sets the text content if this object is an editable field."""
return self._ui.setText(self._selector.to_dict(), text)
@property
def children(self) -> Sequence[byselector.SelectorType]:
"""The child objects directly under this object."""
return self._ui.getChildren(self._selector.to_dict())
@property
def click(self) -> _Click:
"""Clicks on this object."""
return _Click(self._ui, self._selector)
@property
def drag(self) -> _Drag:
"""Drags this object to the specified location."""
return _Drag(self._ui, self._selector)
@property
def exists(self) -> bool:
"""Checks if the this UI object exists."""
is_exists = self._ui.exists(self._selector.to_dict())
if not is_exists and self._raise_error:
raise errors.UiObjectSearchError(
f'Not found Selector{self._selector.to_dict()}', self._device
)
return is_exists
def assert_exists(self, error_msg: str) -> None:
"""Asserts that this object exists before the timeout."""
current_raise_error = self._raise_error
try:
self._raise_error = True
_ = self.exists
except errors.UiObjectSearchError as e:
raise errors.UiObjectSearchError(error_msg, self._device) from e
finally:
self._raise_error = current_raise_error
@property
def fling(self) -> _Gesture:
"""Performs a fling gesture on this object."""
return _Gesture(self._ui, self._selector, 'fling')
@property
def info(self) -> Mapping[str, Union[bool, int, str, Mapping[str, int]]]:
"""Returns all properties of the UI element."""
return self._ui.getObjInfo(self._selector.to_dict())
@property
def pinch(self) -> _Pinch:
"""Performs a pinch gesture on this object."""
return _Pinch(self._ui, self._selector)
@property
def scroll(self) -> _Scroll:
"""Performs a scroll gesture on this object."""
return _Scroll(self._ui, self._selector)
@property
def swipe(self) -> _Gesture:
"""Performs a swipe gesture on this object."""
return _Gesture(self._ui, self._selector, 'swipe')
@property
def wait(self) -> _Wait:
"""Performs wait action on this object."""
return _Wait(self._ui, self._selector, self._raise_error)
@property
def count(self) -> int:
"""The number of objects that match this selector criteria."""
return len(self._ui.findObjects(self._selector.to_dict()))
@property
def display_id(self) -> int:
"""The ID of the display containing this object."""
return self._ui.getDisplayId(self._selector.to_dict())
@property
def class_name(self) -> str:
"""The class name of this object."""
return self._ui.getClassName(self._selector.to_dict())
@property
def description(self) -> str:
"""The content description for this object."""
return self._ui.getContentDescription(self._selector.to_dict())
@property
def hint(self) -> str:
"""The hint text of this object."""
return self._ui.getHint(self._selector.to_dict())
@property
def package_name(self) -> str:
"""The package name of the app that this object belongs to."""
return self._ui.getApplicationPackage(self._selector.to_dict())
@property
def resource_id(self) -> str:
"""The fully qualified resource name for this object's id."""
return self._ui.getResourceName(self._selector.to_dict())
@property
def text(self) -> str:
"""The text value for this object."""
return self._ui.getText(self._selector.to_dict())
@property
def checkable(self) -> bool:
"""Whether this object is checkable."""
return self._ui.isCheckable(self._selector.to_dict())
@property
def checked(self) -> bool:
"""Whether this object is checked."""
return self._ui.isChecked(self._selector.to_dict())
@property
def clickable(self) -> bool:
"""Whether this object is clickable."""
return self._ui.isClickable(self._selector.to_dict())
@property
def enabled(self) -> bool:
"""Whether this object is enabled."""
return self._ui.isEnabled(self._selector.to_dict())
@property
def focusable(self) -> bool:
"""Whether this object is focusable."""
return self._ui.isFocusable(self._selector.to_dict())
@property
def focused(self) -> bool:
"""Whether this object is focused."""
return self._ui.isFocused(self._selector.to_dict())
@property
def long_clickable(self) -> bool:
"""Whether this object is long clickable."""
return self._ui.isLongClickable(self._selector.to_dict())
@property
def scrollable(self) -> bool:
"""Whether this object is scrollable."""
return self._ui.isScrollable(self._selector.to_dict())
@property
def selected(self) -> bool:
"""Whether this object is selected."""
return self._ui.isSelected(self._selector.to_dict())
@property
def visible_bounds(self) -> constants.Rect:
"""This object's visible bounds."""
rect = self._ui.getVisibleBounds(self._selector.to_dict())
return constants.Rect(**rect)
@property
def visible_center(self) -> constants.Point:
"""The point in the center of this object's visible bounds."""
point = self._ui.getVisibleCenter(self._selector.to_dict())
return constants.Point(**point)
@property
def selector(self) -> byselector.BySelector:
"""The selector used to find this object."""
return self._selector