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 @@
except Exception as e:
pytest.fail(f"Failed to initialize driver: {e}")

yield driver
yield event_driver

Check warning on line 65 in conftest.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unbound local variables

Local variable 'event_driver' might be referenced before assignment

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

Check warning on line 68 in conftest.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unbound local variables

Local variable 'event_driver' might be referenced before assignment


# 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
@@ -1,15 +1,18 @@
from appium.webdriver.common.appiumby import AppiumBy


class Locators:

Check notice on line 4 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Class has no `__init__` method

Class has no __init__ method
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:

Check notice on line 5 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Class has no `__init__` method

Class has no __init__ method

Check notice on line 5 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

PEP 8 naming convention violation

Class names should use CapWords convention
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:

Check notice on line 11 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Class has no `__init__` method

Class has no __init__ method

Check notice on line 11 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

PEP 8 naming convention violation

Class names should use CapWords convention
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:

Check notice on line 17 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Class has no `__init__` method

Class has no __init__ method

Check notice on line 17 in src/locators/locators.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

PEP 8 naming convention violation

Class names should use CapWords convention
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"]

Check warning on line 8 in src/screens/base_screen.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Invalid type hints definitions and usages

'Literal' may be parameterized with literal ints, byte and unicode strings, bools, Enum values, None, other literal types, or type aliases to other literal types
type Direction = Literal["down", "up"]

Check warning on line 9 in src/screens/base_screen.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Invalid type hints definitions and usages

'Literal' may be parameterized with literal ints, byte and unicode strings, bools, Enum values, None, other literal types, or type aliases to other literal types


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 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,19 +92,43 @@

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:
element = self.element(locator, condition=condition, **kwargs)

Check notice on line 131 in src/screens/base_screen.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Local variable 'element' value is not used
# TODO
except Exception as e:
print(f"Error during double tap action: {e}")
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"]

Check warning on line 15 in src/screens/element_interactor.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Invalid type hints definitions and usages

'Literal' may be parameterized with literal ints, byte and unicode strings, bools, Enum values, None, other literal types, or type aliases to other literal types


class WaitType(Enum):
Expand All @@ -26,7 +27,7 @@
self.driver = driver
self.waiters = {
wait_type: WebDriverWait(driver, wait_type.value)
for wait_type in WaitType

Check warning on line 30 in src/screens/element_interactor.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Incorrect type

Expected type 'collections.Iterable', got 'Type\[WaitType\]' instead
if wait_type != WaitType.FLUENT
}
self.waiters[WaitType.FLUENT] = WebDriverWait(
Expand All @@ -34,19 +35,20 @@
)

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 @@ -54,14 +56,14 @@
return waiter.until(conditions[condition])
except TimeoutException as e:
raise TimeoutException(
f"Condition '{condition}' failed for element {locator} after {waiter._timeout} seconds"

Check notice on line 59 in src/screens/element_interactor.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Accessing a protected member of a class or a module

Access to a protected member _timeout of a class
) from e

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 @@
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 @@
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 @@
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 @@
"""
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"):

Check warning on line 18 in src/screens/main_screen/main_screen.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Invalid type hints definitions and usages

'Literal' may be parameterized with literal ints, byte and unicode strings, bools, Enum values, None, other literal types, or type aliases to other literal types
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()
Loading