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
2 changes: 1 addition & 1 deletion config/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ stage:
autoGrantPermissions: True
appWaitForLaunch: True
maxRetryCount: 40
noReset: True
noReset: False
appWaitDuration: 30000
appPackage: "io.appium.android.apis"
appActivity: "io.appium.android.apis.ApiDemos"
Expand Down
6 changes: 3 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@
try:
e_listener = AppEventListener()
driver = Driver.get_driver(platform)
event_driver = EventFiringWebDriver(driver, e_listener)

Check notice on line 61 in conftest.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Local variable 'event_driver' value is not used
except Exception as e:
pytest.fail(f"Failed to initialize driver: {e}")

yield event_driver
yield 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 'driver' might be referenced before assignment

if event_driver is not None:
event_driver.quit()
if driver is not None:
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 'driver' might be referenced before assignment


# def pytest_runtest_makereport(item, call):
Expand Down
2 changes: 1 addition & 1 deletion src/drivers/event_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@
class AppEventListener(AbstractEventListener):
"""Custom Event Listener for Appium WebDriver."""

def before_find(self, by, value, driver):

Check notice on line 13 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `before_find` may be 'static'

Check notice on line 13 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info(f"Looking for element: {by} -> {value}")

def after_find(self, by, value, driver):

Check notice on line 16 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `after_find` may be 'static'

Check notice on line 16 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info(f"Found element: {by} -> {value}")

def before_click(self, element, driver):

Check notice on line 19 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `before_click` may be 'static'

Check notice on line 19 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info(f"Before clicking: {element}")

def after_click(self, element, driver):

Check notice on line 22 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `after_click` may be 'static'

Check notice on line 22 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info(f"Clicked on: {element}")

def before_quit(self, driver):

Check notice on line 25 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `before_quit` may be 'static'

Check notice on line 25 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info("Driver is about to quit.")

def after_quit(self, driver):

Check notice on line 28 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `after_quit` may be 'static'

Check notice on line 28 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info("Driver has quit.")

def on_exception(self, exception, driver) -> None:

Check notice on line 31 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Method is not declared static

Method `on_exception` may be 'static'

Check notice on line 31 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'exception' value is not used

Check notice on line 31 in src/drivers/event_listener.py

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

Unused local symbols

Parameter 'driver' value is not used
logger.info(f"On exception")
logger.info(f"On exception")
Empty file added src/locators/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions src/locators/locators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from appium.webdriver.common.appiumby import AppiumBy


class Common:

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
text_link = (AppiumBy.ACCESSIBILITY_ID, 'Text')
content_link = (AppiumBy.ACCESSIBILITY_ID, 'Content')
menu_elements = (AppiumBy.XPATH, '//android.widget.TextView')
26 changes: 18 additions & 8 deletions src/screens/base_screen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import time
from typing import Tuple
from typing import Tuple, Literal

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


Locator = Tuple[str, str]

Expand All @@ -10,12 +12,20 @@
def __init__(self, driver):
super().__init__(driver)

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

Check warning on line 18 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
):
element = self.element(locator, condition=condition)
element.click()

def tap(self, locator, **kwargs):
element = self.element(locator, condition="clickable", **kwargs)
self.driver.tap()
action_helpers = ActionHelpers()
action_helpers.tap(element)

def tap(self):
pass

def tap_by_coordinates(self):
pass

Expand All @@ -40,7 +50,7 @@

def get_screen_size(self):
return self.driver.get_window_size()

def back(self):
self.driver.back()

Expand All @@ -51,4 +61,4 @@
self.driver.reset()

def launch_app(self):
self.driver.launch_app()
self.driver.launch_app()
171 changes: 46 additions & 125 deletions src/screens/element_interactor.py
Original file line number Diff line number Diff line change
@@ -1,116 +1,83 @@
import time
from enum import Enum
from typing import Tuple, Optional, Literal, List
import time
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

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

View workflow job for this annotation

GitHub Actions / Qodana Community for Python

PEP 8 naming convention violation

Lowercase variable imported as non-lowercase
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import (
TimeoutException,
ElementNotVisibleException,
NoSuchElementException,
)
from selenium.common.exceptions import TimeoutException, NoSuchElementException

Locator = Tuple[str, str]


class WaitType(Enum):
"""
Enumeration for different wait durations used in WebDriverWait.
"""

DEFAULT = 30
SHORT = 5
LONG = 60
FLUENT = 10


class ElementInteractor:
"""
A utility class for interacting with screen elements, waits strategy.
"""

def __init__(self, driver):
"""
Initializes the ElementInteractor with a WebDriver instance and predefined waiters.

:param driver: The Selenium WebDriver instance to interact with.
:type driver: WebDriver
"""
self.driver = driver
self.waiters = {
WaitType.DEFAULT: WebDriverWait(driver, WaitType.DEFAULT.value),
WaitType.SHORT: WebDriverWait(driver, WaitType.SHORT.value),
WaitType.LONG: WebDriverWait(driver, WaitType.LONG.value),
WaitType.FLUENT: WebDriverWait(
driver,
WaitType.FLUENT.value,
poll_frequency=1,
ignored_exceptions=[ElementNotVisibleException],
),
wait_type: WebDriverWait(driver, wait_type.value)
for wait_type in WaitType

Check warning on line 24 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(
driver, WaitType.FLUENT.value, poll_frequency=1
)

def _get_waiter(self, wait_type: Optional[WaitType] = None) -> WebDriverWait:
"""
Returns the appropriate WebDriverWait instance based on the specified wait type.

:param wait_type: The type of wait (default is `WaitType.DEFAULT`).
:type wait_type: Optional[WaitType]

:return: The WebDriverWait instance for the specified wait type.
:rtype: WebDriverWait
"""
return self.waiters.get(wait_type, self.waiters[WaitType.DEFAULT])

def wait_for(
self,
locator: Locator,
condition: Literal["clickable", "visible", "present"] = "visible",

Check warning on line 37 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
waiter: Optional[WebDriverWait] = None,
) -> WebElement:
"""
Waits for an element to meet the specified condition.

:param locator: A tuple containing the strategy and value of the element locator.
:param condition: The condition to wait for ("clickable", "visible", or "present").
:param waiter: A custom WebDriverWait instance. Defaults to `None`, which uses the default waiter.

:return: The located web element once the condition is satisfied.
"""
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}")

try:
return waiter.until(conditions[condition])
except TimeoutException as e:
raise TimeoutException(
f"Condition '{condition}' failed for element {locator} "
f"after {waiter._timeout} seconds"
f"Condition '{condition}' failed for element {locator} after {waiter._timeout} seconds"

Check notice on line 52 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",

Check warning on line 59 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
wait_type: Optional[WaitType] = WaitType.DEFAULT,
):
for attempt in range(1, n + 1):
try:
self.wait_for(
locator, condition=condition, waiter=self._get_waiter(wait_type)
)
return self.driver.find_element(*locator)
except NoSuchElementException:
if attempt == n:
raise NoSuchElementException(
f"Could not locate element with value: {locator}"
)

def elements(
self,
locator: Locator,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",

Check warning on line 78 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
wait_type: Optional[WaitType] = WaitType.DEFAULT,
) -> List[WebElement]:
"""
Attempts to locate a list of elements by polling a maximum of 'n' times.

:param locator: A tuple containing the strategy and value of the element locator.
:param n: The maximum number of attempts to find the elements. Default is 3.
:param condition: The condition to wait for ("clickable", "visible", or "present").
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.

:return: A list of located web elements that match the condition.
"""
for attempt in range(1, n + 1):
try:
self.wait_for(
Expand All @@ -122,53 +89,15 @@
raise NoSuchElementException(
f"Could not locate element list with value: {locator}"
)
except Exception:
if attempt == n:
raise

def _assert_element_displayed(self, element: WebElement, expected: bool) -> None:
"""
Asserts that the element's displayed status matches the expected value.

:param element: The web element to check.
:param expected: The expected visibility status of the element (True or False).

:raises AssertionError: If the element's visibility does not match the expected value.
"""
assert element.is_displayed() == expected

def _check_elements_displayed(
self, elements: List[WebElement], expected: bool, index: Optional[int] = None
) -> bool:
"""
Checks if the elements are displayed and if applicable, checks a specific element by index.

:param elements: The list of web elements to check.
:param expected: The expected visibility status of the elements (True or False).
:param index: The index of the specific element to check. If `None`, all elements are checked.
:return: True if the element(s) are displayed with the expected status, otherwise False.
"""
if index is None:
return all(e.is_displayed() == expected for e in elements)
return elements[index].is_displayed() == expected

def is_displayed(
self,
locator: Locator,
expected: bool = True,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",

Check warning on line 98 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
wait_type: Optional[WaitType] = None,
) -> None:
"""Checks for an element to be displayed or not, and asserts the visibility.

:param locator: A tuple containing the strategy and value of the element locator.
:param expected: The expected visibility status of the element (True or False).
:param n: The maximum number of attempts to check visibility. Defaults to 3.
:param condition: The condition to wait for ("clickable", "visible", or "present").
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.

:raises AssertionError: If the element's visibility does not match the expected value"""
wait_type = wait_type or WaitType.DEFAULT
for _ in range(n):
try:
Expand All @@ -179,39 +108,31 @@
return
except Exception:
time.sleep(0.5)
assert False == expected
if expected: # Assert if the element is expected to be displayed but isn't
raise AssertionError(f"Element {locator} was not displayed as expected.")
else: # Assert if the element should not be displayed but is
raise AssertionError(
f"Element {locator} was displayed when it shouldn't be."
)

def is_exist(
self,
locator: Locator,
expected: bool = True,
n: int = 3,
condition: Literal["clickable", "visible", "present"] = "visible",

Check warning on line 123 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
wait_type: Optional[WaitType] = WaitType.DEFAULT,
**kwargs,
) -> bool:
"""
Checks for an element's existence and checks if it meets the expected visibility status.

:param locator: A tuple containing the strategy and value of the element locator.
:param expected: The expected existence status of the element (True or False).
:param n: The maximum number of attempts to check existence. Defaults to 3.
:param condition: The condition to wait for ("clickable", "visible", or "present").
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.
:param **kwargs: Additional keyword arguments, such as `index` for checking a specific element in a list.

:return: `True` if the element(s) exist and match the expected visibility status, otherwise `False`.
:rtype: bool
"""
for _ in range(n):
try:
elements = self.wait_for(
locator, condition=condition, waiter=self._get_waiter(wait_type)
element = self.element(
locator, n=1, condition=condition, wait_type=wait_type
)
if isinstance(elements, list) and self._check_elements_displayed(
elements, expected, kwargs.get("index")
):
return element.is_displayed() == expected
except NoSuchElementException:
if not expected:
return True
except Exception:
if _ == n - 1:
return False
pass
time.sleep(0.5)
return not expected
Empty file added src/screens/main/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions src/screens/main/main_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from locators.locators import Common
from screens.base_screen import Screen


class MainScreen(Screen):

def __init__(self, driver):
super().__init__(driver)

def click_on_text_link(self):
self.click(locator = Common.text_link)
Loading
Loading