Skip to content

Commit c209456

Browse files
author
dmy.berezovskyi
committed
added: ability to scroll to element and wait till element is visible
- Updated `scroll_until_element_visible` to first find the elements using locators before passing them to the `scroll` function. - Enhanced scrolling logic to ensure elements are properly located and scrolled into view. chore: updated ruff configuration to exclude docstring checks - Modified `.ruff.toml` to exclude docstring-related linting rules.
1 parent 5e7c097 commit c209456

6 files changed

Lines changed: 118 additions & 49 deletions

File tree

.ruff.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ ignore = [
2424
"N801", # Function name should be lowercase
2525
"I001", # Import convention violation
2626
"F631", # Assert should not be used with a literal
27+
"D212", # Multi-line docstring should start on the first line
28+
"D213", # Multi-line docstring should start on the second line
29+
"E501", # Line too long
2730
]
2831

2932
# Regular expression for dummy variables

src/locators/locators.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class main_menu:
1010

1111
class views_menu:
1212
TEXT_FIELDS = (AppiumBy.ACCESSIBILITY_ID, 'TextFields')
13+
ANIMATION_LINK = (AppiumBy.ACCESSIBILITY_ID, 'Animation')
14+
GALLERY_LINK = (AppiumBy.ACCESSIBILITY_ID, 'Gallery')
15+
IMAGE_BUTTON = (AppiumBy.ACCESSIBILITY_ID, 'ImageButton')
1316

1417
class views_fields:
1518
HINT_INPUT = (AppiumBy.ACCESSIBILITY_ID, 'hint')

src/screens/base_screen.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import time
2-
from typing import Tuple, Literal, Optional
3-
4-
from selenium.webdriver.common.actions import interaction
5-
from selenium.webdriver.common.actions.action_builder import ActionBuilder
6-
from selenium.webdriver.common.actions.pointer_actions import PointerActions
7-
from selenium.webdriver.common.actions.pointer_input import PointerInput
2+
from typing import Tuple, Literal
83

94
from screens.element_interactor import ElementInteractor
10-
from appium.webdriver.extensions.action_helpers import ActionHelpers, ActionChains
115

126

137
Locator = Tuple[str, str]
@@ -27,7 +21,7 @@ def click(
2721

2822
def tap(self, locator: Locator, duration: float = 500, **kwargs):
2923
"""Taps on an element using ActionHelpers.
30-
Taps on an particular place with up to five fingers, holding for a
24+
Taps on a particular place with up to five fingers, holding for a
3125
certain duration
3226
3327
:param locator: locator of an element
@@ -72,7 +66,7 @@ def scroll(
7266
end_ratio: float = 0.3,
7367
):
7468
"""
75-
Scrolls down the screen with customizable scroll size.
69+
Scrolls down/up the screen with customizable scroll size.
7670
7771
:param directions: up or down:
7872
:param start_ratio: Percentage (0-1) from where the scroll starts
@@ -100,6 +94,33 @@ def scroll(
10094

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

97+
def scroll_to_element(
98+
self, from_el: Locator, destination_el: Locator, duration: [int] = 500
99+
):
100+
"""Scrolls to the destination element(Both elements must be located(visible)).
101+
102+
:param from_el: Locator of the element to start scrolling from.
103+
:param destination_el: Locator of the target element to scroll to.
104+
:param duration: Optional duration for each scroll.
105+
"""
106+
from_element = self.element(from_el)
107+
to_element = self.element(destination_el)
108+
109+
self.driver.scroll(to_element, from_element, duration=duration)
110+
111+
def scroll_until_element_visible(
112+
self,
113+
destination_el: Locator,
114+
directions: Literal["down", "up"] = "down",
115+
start_ratio: float = 0.6,
116+
end_ratio: float = 0.3,
117+
retries: int = 1,
118+
):
119+
while self.is_exist(destination_el, expected=False, n=retries):
120+
self.scroll(
121+
directions=directions, start_ratio=start_ratio, end_ratio=end_ratio
122+
)
123+
103124
def type(self, locator: Locator, text: str):
104125
element = self.element(locator)
105126
element.send_keys(text)

src/screens/element_interactor.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -126,29 +126,54 @@ def is_exist(
126126
expected: bool = True,
127127
n: int = 3,
128128
condition: Literal["clickable", "visible", "present"] = "visible",
129-
wait_type: Optional[WaitType] = WaitType.DEFAULT,
129+
wait_type: Optional[WaitType] = WaitType.SHORT,
130+
retry_delay: float = 0.5,
130131
) -> bool:
132+
"""
133+
Checks if an element exists on the screen within a specified number of retries.
134+
135+
:param retry_delay: delay between retry
136+
:param locator: The locator tuple (strategy, value) used to find the element.
137+
:param expected: Determines whether the element should exist (True) or not (False).
138+
:param n: The number of attempts to check for the element before returning a result.
139+
:param condition: The condition to check for the element's existence.
140+
- "clickable": Ensures the element is interactable.
141+
- "visible": Ensures the element is visible on the page.
142+
- "present": Ensures the element exists in the DOM (even if not visible).
143+
:param wait_type: Specifies the wait strategy (default is WaitType.DEFAULT).
144+
:return: True if the element matches the expected state, False otherwise.
145+
:rtype: bool
146+
147+
148+
**Usage Example:**
149+
150+
screen.is_exist(("id", "login-button"))
151+
True
152+
153+
screen.is_exist(("id", "error-popup"), expected=False)
154+
True
155+
"""
131156
for _ in range(n):
132157
try:
133158
element = self.element(
134159
locator, n=1, condition=condition, wait_type=wait_type
135160
)
136161
return element.is_displayed() == expected
137-
except NoSuchElementException:
162+
except (NoSuchElementException, TimeoutException):
138163
if not expected:
139164
return True
140-
except Exception:
141-
pass
142-
time.sleep(0.5)
165+
except Exception as e:
166+
print(f"Unexpected error in is_exist: {e}")
167+
time.sleep(retry_delay)
143168
return not expected
144-
169+
145170
def scroll_by_coordinates(
146-
self,
147-
start_x: int,
148-
start_y: int,
149-
end_x: int,
150-
end_y: int,
151-
duration: Optional[int] = None,
171+
self,
172+
start_x: int,
173+
start_y: int,
174+
end_x: int,
175+
end_y: int,
176+
duration: Optional[int] = None,
152177
):
153178
"""Scrolls from one set of coordinates to another.
154179
@@ -161,17 +186,18 @@ def scroll_by_coordinates(
161186
"""
162187
if duration is None:
163188
duration = 700
164-
189+
165190
touch_input = PointerInput(interaction.POINTER_TOUCH, "touch")
166191
actions = ActionChains(self.driver)
167-
168-
actions.w3c_actions = ActionBuilder(self.driver, mouse = touch_input)
192+
193+
actions.w3c_actions = ActionBuilder(self.driver, mouse=touch_input)
169194
actions.w3c_actions.pointer_action.move_to_location(start_x, start_y)
170195
actions.w3c_actions.pointer_action.pointer_down()
171-
actions.w3c_actions = ActionBuilder(self.driver, mouse=touch_input, duration=duration)
172-
196+
actions.w3c_actions = ActionBuilder(
197+
self.driver, mouse=touch_input, duration=duration
198+
)
199+
173200
actions.w3c_actions.pointer_action.move_to_location(end_x, end_y)
174201
actions.w3c_actions.pointer_action.release()
175-
202+
176203
actions.perform()
177-

src/screens/main_screen/main_screen.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,29 @@
55

66

77
class MainScreen(Screen):
8-
9-
def __init__(self, driver):
10-
super().__init__(driver)
11-
self.locators = Locators()
12-
13-
def click_on_text_link(self):
14-
self.click(locator = self.locators.main_menu.TEXT_LINK)
15-
16-
def tap_on_text_link(self):
17-
self.tap(locator = self.locators.main_menu.TEXT_LINK)
18-
19-
def scroll_view_by_coordinates(self, direction: Literal['down', 'up'] = 'down'):
20-
self.tap(locator = self.locators.main_menu.VIEWS_LINK)
21-
self.scroll(directions = direction)
22-
23-
8+
def __init__(self, driver):
9+
super().__init__(driver)
10+
self.locators = Locators()
11+
12+
def click_on_text_link(self):
13+
self.click(locator=self.locators.main_menu.TEXT_LINK)
14+
15+
def tap_on_text_link(self):
16+
self.tap(locator=self.locators.main_menu.TEXT_LINK)
17+
18+
def scroll_view_by_coordinates(self, direction: Literal["down", "up"] = "down"):
19+
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
20+
self.scroll(directions=direction)
21+
22+
def scroll_to_image_button(self):
23+
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
24+
self.scroll_to_element(
25+
from_el=self.locators.views_menu.ANIMATION_LINK,
26+
destination_el=self.locators.views_menu.IMAGE_BUTTON,
27+
)
28+
29+
def scroll_until_text_field_visible(self):
30+
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
31+
self.scroll_until_element_visible(
32+
destination_el=self.locators.views_menu.TEXT_FIELDS
33+
)

tests/test_p1/test_actions.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ class TestClick:
77
def setup(self, driver) -> None:
88
"""Setup common objects for tests after address is set."""
99
self.main_screen = MainScreen(driver)
10-
10+
1111
def test_click(self, setup):
1212
self.main_screen.click_on_text_link()
13-
13+
1414
def test_tap(self, setup):
1515
self.main_screen.tap_on_text_link()
16-
16+
1717
def test_scroll_by_coordinates(self, setup):
18-
self.main_screen.scroll_view_by_coordinates(direction = "down")
19-
self.main_screen.scroll('up')
18+
self.main_screen.scroll_view_by_coordinates(direction="down")
19+
self.main_screen.scroll("up")
20+
21+
def test_sroll_to_element(self, setup):
22+
self.main_screen.scroll_to_image_button()
23+
24+
def test_scroll_util_visible(self, setup):
25+
self.main_screen.scroll_until_text_field_visible()

0 commit comments

Comments
 (0)