Skip to content

Commit af6cf68

Browse files
Merge pull request #5 from dmberezovskyii/PPA-0004
PPA-0004: added element_interactor.py
2 parents 2006735 + 1df339d commit af6cf68

File tree

3 files changed

+274
-2
lines changed

3 files changed

+274
-2
lines changed

conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ def device(request):
5050
@pytest.fixture(scope="function")
5151
def driver(request):
5252
platform = request.config.getoption("--platform")
53+
driver = None
54+
5355
try:
5456
driver = Driver.get_driver(platform)
5557
except Exception as e:
5658
pytest.fail(f"Failed to initialize driver: {e}")
5759

5860
yield driver
61+
5962
if driver is not None:
6063
driver.quit()

src/screens/base_screen.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,54 @@
1-
class Screen:
2-
pass
1+
import time
2+
from typing import Tuple
3+
4+
from screens.element_interactor import ElementInteractor
5+
6+
Locator = Tuple[str, str]
7+
8+
9+
class Screen(ElementInteractor):
10+
def __init__(self, driver):
11+
super().__init__(driver)
12+
13+
def click(self):
14+
pass
15+
16+
def tap(self):
17+
pass
18+
19+
def tap_by_coordinates(self):
20+
pass
21+
22+
def swipe(self):
23+
pass
24+
25+
def type(self):
26+
pass
27+
28+
def double_tap(self):
29+
pass
30+
31+
def long_press(self):
32+
pass
33+
34+
@staticmethod
35+
def sleep(kwargs):
36+
try:
37+
time.sleep(kwargs["sleep"])
38+
except KeyError:
39+
pass
40+
41+
def get_screen_size(self):
42+
return self.driver.get_window_size()
43+
44+
def back(self):
45+
self.driver.back()
46+
47+
def close(self):
48+
self.driver.close_app()
49+
50+
def reset(self):
51+
self.driver.reset()
52+
53+
def launch_app(self):
54+
self.driver.launch_app()

src/screens/element_interactor.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import time
2+
from enum import Enum
3+
from typing import Tuple, Optional, Literal, List
4+
from selenium.webdriver.remote.webelement import WebElement
5+
from selenium.webdriver.support import expected_conditions as ec
6+
from selenium.webdriver.support.wait import WebDriverWait
7+
from selenium.common.exceptions import (
8+
TimeoutException,
9+
ElementNotVisibleException,
10+
NoSuchElementException,
11+
)
12+
13+
Locator = Tuple[str, str]
14+
15+
16+
class WaitType(Enum):
17+
"""
18+
Enumeration for different wait durations used in WebDriverWait.
19+
"""
20+
21+
DEFAULT = 30
22+
SHORT = 5
23+
LONG = 60
24+
FLUENT = 10
25+
26+
27+
class ElementInteractor:
28+
"""
29+
A utility class for interacting with screen elements, waits strategy.
30+
"""
31+
32+
def __init__(self, driver):
33+
"""
34+
Initializes the ElementInteractor with a WebDriver instance and predefined waiters.
35+
36+
:param driver: The Selenium WebDriver instance to interact with.
37+
:type driver: WebDriver
38+
"""
39+
self.driver = driver
40+
self.waiters = {
41+
WaitType.DEFAULT: WebDriverWait(driver, WaitType.DEFAULT.value),
42+
WaitType.SHORT: WebDriverWait(driver, WaitType.SHORT.value),
43+
WaitType.LONG: WebDriverWait(driver, WaitType.LONG.value),
44+
WaitType.FLUENT: WebDriverWait(
45+
driver,
46+
WaitType.FLUENT.value,
47+
poll_frequency=1,
48+
ignored_exceptions=[ElementNotVisibleException],
49+
),
50+
}
51+
52+
def _get_waiter(self, wait_type: Optional[WaitType] = None) -> WebDriverWait:
53+
"""
54+
Returns the appropriate WebDriverWait instance based on the specified wait type.
55+
56+
:param wait_type: The type of wait (default is `WaitType.DEFAULT`).
57+
:type wait_type: Optional[WaitType]
58+
59+
:return: The WebDriverWait instance for the specified wait type.
60+
:rtype: WebDriverWait
61+
"""
62+
return self.waiters.get(wait_type, self.waiters[WaitType.DEFAULT])
63+
64+
def wait_for(
65+
self,
66+
locator: Locator,
67+
condition: Literal["clickable", "visible", "present"] = "visible",
68+
waiter: Optional[WebDriverWait] = None,
69+
) -> WebElement:
70+
"""
71+
Waits for an element to meet the specified condition.
72+
73+
:param locator: A tuple containing the strategy and value of the element locator.
74+
:param condition: The condition to wait for ("clickable", "visible", or "present").
75+
:param waiter: A custom WebDriverWait instance. Defaults to `None`, which uses the default waiter.
76+
77+
:return: The located web element once the condition is satisfied.
78+
"""
79+
waiter = waiter or self._get_waiter()
80+
conditions = {
81+
"clickable": ec.element_to_be_clickable(locator),
82+
"visible": ec.visibility_of_element_located(locator),
83+
"present": ec.presence_of_element_located(locator),
84+
}
85+
86+
if condition not in conditions:
87+
raise ValueError(f"Unknown condition: {condition}")
88+
89+
try:
90+
return waiter.until(conditions[condition])
91+
except TimeoutException as e:
92+
raise TimeoutException(
93+
f"Condition '{condition}' failed for element {locator} "
94+
f"after {waiter._timeout} seconds"
95+
) from e
96+
97+
def elements(
98+
self,
99+
locator: Locator,
100+
n: int = 3,
101+
condition: Literal["clickable", "visible", "present"] = "visible",
102+
wait_type: Optional[WaitType] = WaitType.DEFAULT,
103+
) -> List[WebElement]:
104+
"""
105+
Attempts to locate a list of elements by polling a maximum of 'n' times.
106+
107+
:param locator: A tuple containing the strategy and value of the element locator.
108+
:param n: The maximum number of attempts to find the elements. Default is 3.
109+
:param condition: The condition to wait for ("clickable", "visible", or "present").
110+
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.
111+
112+
:return: A list of located web elements that match the condition.
113+
"""
114+
for attempt in range(1, n + 1):
115+
try:
116+
self.wait_for(
117+
locator, condition=condition, waiter=self._get_waiter(wait_type)
118+
)
119+
return self.driver.find_elements(*locator)
120+
except NoSuchElementException:
121+
if attempt == n:
122+
raise NoSuchElementException(
123+
f"Could not locate element list with value: {locator}"
124+
)
125+
except Exception:
126+
if attempt == n:
127+
raise
128+
129+
def _assert_element_displayed(self, element: WebElement, expected: bool) -> None:
130+
"""
131+
Asserts that the element's displayed status matches the expected value.
132+
133+
:param element: The web element to check.
134+
:param expected: The expected visibility status of the element (True or False).
135+
136+
:raises AssertionError: If the element's visibility does not match the expected value.
137+
"""
138+
assert element.is_displayed() == expected
139+
140+
def _check_elements_displayed(
141+
self, elements: List[WebElement], expected: bool, index: Optional[int] = None
142+
) -> bool:
143+
"""
144+
Checks if the elements are displayed and if applicable, checks a specific element by index.
145+
146+
:param elements: The list of web elements to check.
147+
:param expected: The expected visibility status of the elements (True or False).
148+
:param index: The index of the specific element to check. If `None`, all elements are checked.
149+
:return: True if the element(s) are displayed with the expected status, otherwise False.
150+
"""
151+
if index is None:
152+
return all(e.is_displayed() == expected for e in elements)
153+
return elements[index].is_displayed() == expected
154+
155+
def is_displayed(
156+
self,
157+
locator: Locator,
158+
expected: bool = True,
159+
n: int = 3,
160+
condition: Literal["clickable", "visible", "present"] = "visible",
161+
wait_type: Optional[WaitType] = None,
162+
) -> None:
163+
"""Checks for an element to be displayed or not, and asserts the visibility.
164+
165+
:param locator: A tuple containing the strategy and value of the element locator.
166+
:param expected: The expected visibility status of the element (True or False).
167+
:param n: The maximum number of attempts to check visibility. Defaults to 3.
168+
:param condition: The condition to wait for ("clickable", "visible", or "present").
169+
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.
170+
171+
:raises AssertionError: If the element's visibility does not match the expected value"""
172+
wait_type = wait_type or WaitType.DEFAULT
173+
for _ in range(n):
174+
try:
175+
element = self.wait_for(
176+
locator, condition=condition, waiter=self._get_waiter(wait_type)
177+
)
178+
assert element.is_displayed() == expected
179+
return
180+
except Exception:
181+
time.sleep(0.5)
182+
assert False == expected
183+
184+
def is_exist(
185+
self,
186+
locator: Locator,
187+
expected: bool = True,
188+
n: int = 3,
189+
condition: Literal["clickable", "visible", "present"] = "visible",
190+
wait_type: Optional[WaitType] = WaitType.DEFAULT,
191+
**kwargs,
192+
) -> bool:
193+
"""
194+
Checks for an element's existence and checks if it meets the expected visibility status.
195+
196+
:param locator: A tuple containing the strategy and value of the element locator.
197+
:param expected: The expected existence status of the element (True or False).
198+
:param n: The maximum number of attempts to check existence. Defaults to 3.
199+
:param condition: The condition to wait for ("clickable", "visible", or "present").
200+
:param wait_type: The wait type to use for polling. Defaults to `WaitType.DEFAULT`.
201+
:param **kwargs: Additional keyword arguments, such as `index` for checking a specific element in a list.
202+
203+
:return: `True` if the element(s) exist and match the expected visibility status, otherwise `False`.
204+
:rtype: bool
205+
"""
206+
for _ in range(n):
207+
try:
208+
elements = self.wait_for(
209+
locator, condition=condition, waiter=self._get_waiter(wait_type)
210+
)
211+
if isinstance(elements, list) and self._check_elements_displayed(
212+
elements, expected, kwargs.get("index")
213+
):
214+
return True
215+
except Exception:
216+
if _ == n - 1:
217+
return False

0 commit comments

Comments
 (0)