From 7ce7f9ac8dea1b23813fa04973615553dc903749 Mon Sep 17 00:00:00 2001 From: "dmy.berezovskyi" Date: Thu, 13 Mar 2025 00:21:05 +0200 Subject: [PATCH] added ability to scroll down and up by coordinates --- src/locators/locators.py | 16 ++++++-- src/screens/base_screen.py | 55 ++++++++++++++++++++++---- src/screens/element_interactor.py | 44 ++++++++++++++++++++- src/screens/main_screen/main_screen.py | 15 +++++-- tests/test_p1/test_actions.py | 6 ++- 5 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/locators/locators.py b/src/locators/locators.py index b918a81..fec3cdd 100644 --- a/src/locators/locators.py +++ b/src/locators/locators.py @@ -1,7 +1,15 @@ from appium.webdriver.common.appiumby import AppiumBy -class Common: - text_link = (AppiumBy.ACCESSIBILITY_ID, 'Text') - content_link = (AppiumBy.ACCESSIBILITY_ID, 'Content') - menu_elements = (AppiumBy.XPATH, '//android.widget.TextView') \ No newline at end of file +class Locators: + class main_menu: + TEXT_LINK = (AppiumBy.ACCESSIBILITY_ID, 'Text') + CONTENT_LINK = (AppiumBy.ACCESSIBILITY_ID, 'Content') + VIEWS_LINK = (AppiumBy.ACCESSIBILITY_ID, 'Views') + MENU_ELEMENTS = (AppiumBy.XPATH, '//android.widget.TextView') + + class views_menu: + TEXT_FIELDS = (AppiumBy.ACCESSIBILITY_ID, 'TextFields') + + class views_fields: + HINT_INPUT = (AppiumBy.ACCESSIBILITY_ID, 'hint') \ No newline at end of file diff --git a/src/screens/base_screen.py b/src/screens/base_screen.py index 8583d46..6fac3a1 100644 --- a/src/screens/base_screen.py +++ b/src/screens/base_screen.py @@ -1,8 +1,10 @@ import time -from typing import Tuple, Literal +from typing import Tuple, Literal, Optional +from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.pointer_actions import PointerActions +from selenium.webdriver.common.actions.pointer_input import PointerInput from screens.element_interactor import ElementInteractor from appium.webdriver.extensions.action_helpers import ActionHelpers, ActionChains @@ -23,15 +25,20 @@ def click( element = self.element(locator, condition=condition) element.click() - def tap(self, locator, **kwargs): - """Taps on an element using ActionHelpers.""" + def tap(self, locator: Locator, duration: float = 500, **kwargs): + """Taps on an element using ActionHelpers. + Taps on an particular place with up to five fingers, holding for a + certain duration + + :param locator: locator of an element + :param duration: length of time to tap, in ms""" try: element = self.element(locator, condition="clickable", **kwargs) location = element.location size = element.size x = location["x"] + size["width"] // 2 y = location["y"] + size["height"] // 2 - self.driver.tap([(x, y)]) + self.driver.tap([(x, y)], duration=duration) except Exception as e: print(f"Error during tap action: {e}") @@ -43,7 +50,7 @@ def swipe( relative_end_y: float, duration_ms: int = 200, ) -> None: - size = self.driver.get_window_size() + size = self.get_screen_size() width = size["width"] height = size["height"] start_x = int(width * relative_start_x) @@ -58,6 +65,41 @@ def swipe( duration_ms=duration_ms, ) + def scroll( + self, + directions: Literal["down", "up"] = "down", + start_ratio: float = 0.7, + end_ratio: float = 0.3, + ): + """ + Scrolls down the screen with customizable scroll size. + + :param directions: up or down: + :param start_ratio: Percentage (0-1) from where the scroll starts + :type end_ratio: Percentage (0-1) where the scroll ends. + USAGE: + DOWN example + start_y = int(height * 0.7) + end_y = int(height * 0.3) + UP example + start_y = int(height * 0.3) + end_y = int(height * 0.7) + """ + size = self.get_screen_size() + width = size["width"] + height = size["height"] + start_x = width // 2 + if directions == "down": + start_y = int(height * start_ratio) + end_y = int(height * end_ratio) + elif directions == "up": + start_y = int(height * end_ratio) + end_y = int(height * start_ratio) + else: + raise ValueError("Direction must be 'down' or 'up'") + + self.scroll_by_coordinates(start_x, start_y, start_x, end_y) + def type(self, locator: Locator, text: str): element = self.element(locator) element.send_keys(text) @@ -71,8 +113,7 @@ def double_tap( """Double taps on an element.""" try: element = self.element(locator, condition=condition, **kwargs) - action = ActionHelpers() - action.double_tap(element).perform() + # TODO except Exception as e: print(f"Error during double tap action: {e}") diff --git a/src/screens/element_interactor.py b/src/screens/element_interactor.py index e198ef9..e81539c 100644 --- a/src/screens/element_interactor.py +++ b/src/screens/element_interactor.py @@ -1,6 +1,11 @@ from enum import Enum -from typing import Tuple, Optional, Literal, List +from typing import Tuple, Optional, Literal, List, cast import time + +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 from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait @@ -136,3 +141,40 @@ def is_exist( pass time.sleep(0.5) return not expected + + def scroll_by_coordinates( + self, + start_x: int, + start_y: int, + end_x: int, + end_y: int, + duration: Optional[int] = None, + ): + """Scrolls from one set of coordinates to another. + + Args: + start_x: X coordinate to start scrolling from. + start_y: Y coordinate to start scrolling from. + end_x: X coordinate to scroll to. + end_y: Y coordinate to scroll to. + duration: Defines speed of scroll action. Default is 600 ms. + + Returns: + Self instance. + """ + if duration is None: + duration = 1000 + + touch_input = PointerInput(interaction.POINTER_TOUCH, "touch") + actions = ActionChains(self.driver) + + actions.w3c_actions = ActionBuilder(self.driver, mouse = touch_input) + actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) + actions.w3c_actions.pointer_action.pointer_down() + actions.w3c_actions = ActionBuilder(self.driver, mouse=touch_input, duration=duration) + + actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) + actions.w3c_actions.pointer_action.release() + + actions.perform() + diff --git a/src/screens/main_screen/main_screen.py b/src/screens/main_screen/main_screen.py index ddd3440..5de4a34 100644 --- a/src/screens/main_screen/main_screen.py +++ b/src/screens/main_screen/main_screen.py @@ -1,4 +1,6 @@ -from locators.locators import Common +from typing import Literal + +from locators.locators import Locators from screens.base_screen import Screen @@ -6,9 +8,16 @@ class MainScreen(Screen): def __init__(self, driver): super().__init__(driver) + self.locators = Locators() def click_on_text_link(self): - self.click(locator = Common.text_link) + self.click(locator = self.locators.main_menu.TEXT_LINK) def tap_on_text_link(self): - self.tap(locator = Common.text_link) \ No newline at end of file + self.tap(locator = self.locators.main_menu.TEXT_LINK) + + def scroll_view_by_coordinates(self, direction: Literal['down', 'up'] = 'down'): + self.tap(locator = self.locators.main_menu.VIEWS_LINK) + self.scroll(directions = direction) + + \ No newline at end of file diff --git a/tests/test_p1/test_actions.py b/tests/test_p1/test_actions.py index 628ff90..5b1864e 100644 --- a/tests/test_p1/test_actions.py +++ b/tests/test_p1/test_actions.py @@ -12,4 +12,8 @@ def test_click(self, setup): self.main_screen.click_on_text_link() def test_tap(self, setup): - self.main_screen.tap_on_text_link() \ No newline at end of file + self.main_screen.tap_on_text_link() + + def test_scroll_by_coordinates(self, setup): + self.main_screen.scroll_view_by_coordinates(direction = "down") + self.main_screen.scroll('up') \ No newline at end of file