Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ ignore = [
"N801", # Function name should be lowercase
"I001", # Import convention violation
"F631", # Assert should not be used with a literal
"D212", # Multi-line docstring should start on the first line
"D213", # Multi-line docstring should start on the second line
"E501", # Line too long
]

# Regular expression for dummy variables
Expand Down
6 changes: 3 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ def driver(request):
except Exception as e:
pytest.fail(f"Failed to initialize driver: {e}")

yield driver
yield event_driver

if driver is not None:
driver.quit()
if event_driver is not None:
event_driver.quit()


# def pytest_runtest_makereport(item, call):
Expand Down
25 changes: 14 additions & 11 deletions src/locators/locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@


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')
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")
ANIMATION_LINK = (AppiumBy.ACCESSIBILITY_ID, "Animation")
GALLERY_LINK = (AppiumBy.ACCESSIBILITY_ID, "Gallery")
IMAGE_BUTTON = (AppiumBy.ACCESSIBILITY_ID, "ImageButton")

class views_fields:
HINT_INPUT = (AppiumBy.ACCESSIBILITY_ID, "hint")
54 changes: 35 additions & 19 deletions src/screens/base_screen.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import time
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 typing import Tuple, Literal

from screens.element_interactor import ElementInteractor
from appium.webdriver.extensions.action_helpers import ActionHelpers, ActionChains


Locator = Tuple[str, str]
type Condition = Literal["clickable", "visible", "present"]
type Direction = Literal["down", "up"]


class Screen(ElementInteractor):
def __init__(self, driver):
super().__init__(driver)

def click(
self,
locator: Locator,
condition: Literal["clickable", "visible", "present"] = "clickable",
):
def click(self, locator: Locator, condition: Condition = "clickable"):
element = self.element(locator, condition=condition)
element.click()

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
Taps on a particular place with up to five fingers, holding for a
certain duration

:param locator: locator of an element
Expand Down Expand Up @@ -67,12 +59,12 @@ def swipe(

def scroll(
self,
directions: Literal["down", "up"] = "down",
directions: Direction = "down",
start_ratio: float = 0.7,
end_ratio: float = 0.3,
):
"""
Scrolls down the screen with customizable scroll size.
Scrolls down/up the screen with customizable scroll size.

:param directions: up or down:
:param start_ratio: Percentage (0-1) from where the scroll starts
Expand Down Expand Up @@ -100,15 +92,39 @@ def scroll(

self.scroll_by_coordinates(start_x, start_y, start_x, end_y)

def scroll_to_element(
self, from_el: Locator, destination_el: Locator, duration: [int] = 500
):
"""Scrolls to the destination element(Both elements must be located(visible)).

:param from_el: Locator of the element to start scrolling from.
:param destination_el: Locator of the target element to scroll to.
:param duration: Optional duration for each scroll.
"""
from_element = self.element(from_el)
to_element = self.element(destination_el)

self.driver.scroll(to_element, from_element, duration=duration)

def scroll_until_element_visible(
self,
destination_el: Locator,
directions: Direction = "down",
start_ratio: float = 0.6,
end_ratio: float = 0.3,
retries: int = 1,
):
while self.is_exist(destination_el, expected=False, n=retries):
self.scroll(
directions=directions, start_ratio=start_ratio, end_ratio=end_ratio
)

def type(self, locator: Locator, text: str):
element = self.element(locator)
element.send_keys(text)

def double_tap(
self,
locator: Locator,
condition: Literal["clickable", "visible", "present"] = "clickable",
**kwargs,
self, locator: Locator, condition: Condition = "clickable", **kwargs
):
"""Double taps on an element."""
try:
Expand Down
84 changes: 56 additions & 28 deletions src/screens/element_interactor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
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 import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import TimeoutException, NoSuchElementException

Locator = Tuple[str, str]
type Condition = Literal["clickable", "visible", "present"]


class WaitType(Enum):
Expand All @@ -34,19 +35,20 @@ def __init__(self, driver):
)

def _get_waiter(self, wait_type: Optional[WaitType] = None) -> WebDriverWait:
"""Returns the appropriate waiter based on the given wait_type."""
return self.waiters.get(wait_type, self.waiters[WaitType.DEFAULT])

def wait_for(
self,
locator: Locator,
condition: Literal["clickable", "visible", "present"] = "visible",
condition: Condition = "visible",
waiter: Optional[WebDriverWait] = None,
) -> WebElement:
waiter = waiter or self._get_waiter()
conditions = {
"clickable": EC.element_to_be_clickable(locator),
"visible": EC.visibility_of_element_located(locator),
"present": EC.presence_of_element_located(locator),
"clickable": ec.element_to_be_clickable(locator),
"visible": ec.visibility_of_element_located(locator),
"present": ec.presence_of_element_located(locator),
}
if condition not in conditions:
raise ValueError(f"Unknown condition: {condition}")
Expand All @@ -61,7 +63,7 @@ def element(
self,
locator: Locator,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",
condition: Condition = "visible",
wait_type: Optional[WaitType] = WaitType.DEFAULT,
):
for attempt in range(1, n + 1):
Expand All @@ -80,7 +82,7 @@ def elements(
self,
locator: Locator,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",
condition: Condition = "visible",
wait_type: Optional[WaitType] = WaitType.DEFAULT,
) -> List[WebElement]:
for attempt in range(1, n + 1):
Expand All @@ -100,7 +102,7 @@ def is_displayed(
locator: Locator,
expected: bool = True,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",
condition: Condition = "visible",
wait_type: Optional[WaitType] = None,
) -> None:
wait_type = wait_type or WaitType.DEFAULT
Expand All @@ -125,30 +127,55 @@ def is_exist(
locator: Locator,
expected: bool = True,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",
wait_type: Optional[WaitType] = WaitType.DEFAULT,
condition: Condition = "visible",
wait_type: Optional[WaitType] = WaitType.SHORT,
retry_delay: float = 0.5,
) -> bool:
"""
Checks if an element exists on the screen within a specified number of retries.

:param retry_delay: delay between retry
:param locator: The locator tuple (strategy, value) used to find the element.
:param expected: Determines whether the element should exist (True) or not (False).
:param n: The number of attempts to check for the element before returning a result.
:param condition: The condition to check for the element's existence.
- "clickable": Ensures the element is interactable.
- "visible": Ensures the element is visible on the page.
- "present": Ensures the element exists in the DOM (even if not visible).
:param wait_type: Specifies the wait strategy (default is WaitType.DEFAULT).
:return: True if the element matches the expected state, False otherwise.
:rtype: bool


**Usage Example:**

screen.is_exist(("id", "login-button"))
True

screen.is_exist(("id", "error-popup"), expected=False)
True
"""
for _ in range(n):
try:
element = self.element(
locator, n=1, condition=condition, wait_type=wait_type
)
return element.is_displayed() == expected
except NoSuchElementException:
except (NoSuchElementException, TimeoutException):
if not expected:
return True
except Exception:
pass
time.sleep(0.5)
except Exception as e:
print(f"Unexpected error in is_exist: {e}")
time.sleep(retry_delay)
return not expected

def scroll_by_coordinates(
self,
start_x: int,
start_y: int,
end_x: int,
end_y: int,
duration: Optional[int] = None,
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.

Expand All @@ -161,17 +188,18 @@ def scroll_by_coordinates(
"""
if duration is None:
duration = 700

touch_input = PointerInput(interaction.POINTER_TOUCH, "touch")
actions = ActionChains(self.driver)
actions.w3c_actions = ActionBuilder(self.driver, mouse = touch_input)

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 = 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()

42 changes: 26 additions & 16 deletions src/screens/main_screen/main_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@


class MainScreen(Screen):

def __init__(self, driver):
super().__init__(driver)
self.locators = Locators()

def click_on_text_link(self):
self.click(locator = self.locators.main_menu.TEXT_LINK)

def tap_on_text_link(self):
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)


def __init__(self, driver):
super().__init__(driver)
self.locators = Locators()

def click_on_text_link(self):
self.click(locator=self.locators.main_menu.TEXT_LINK)

def tap_on_text_link(self):
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)

def scroll_to_image_button(self):
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
self.scroll_to_element(
from_el=self.locators.views_menu.ANIMATION_LINK,
destination_el=self.locators.views_menu.IMAGE_BUTTON,
)

def scroll_until_text_field_visible(self):
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
self.scroll_until_element_visible(
destination_el=self.locators.views_menu.TEXT_FIELDS
)
18 changes: 12 additions & 6 deletions tests/test_p1/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
from screens.main_screen.main_screen import MainScreen


class TestClick:
class TestBaseActions:
@pytest.fixture(autouse=True)
def setup(self, driver) -> None:
"""Setup common objects for tests after address is set."""
self.main_screen = MainScreen(driver)

def test_click(self, setup):
self.main_screen.click_on_text_link()

def test_tap(self, setup):
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')
self.main_screen.scroll_view_by_coordinates(direction="down")
self.main_screen.scroll("up")

def test_sroll_to_element(self, setup):
self.main_screen.scroll_to_image_button()

def test_scroll_util_visible(self, setup):
self.main_screen.scroll_until_text_field_visible()