Skip to content

Commit 767c644

Browse files
dmberezovskyiidmy.berezovskyi
andauthored
Added swipe to delete, logging, double tap, screenshot on exception (#13)
Co-authored-by: dmy.berezovskyi <dmy.berezovskyi@foodtech.team>
1 parent 811f38a commit 767c644

File tree

10 files changed

+188
-63
lines changed

10 files changed

+188
-63
lines changed

conftest.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
from drivers.driver_factory import Driver
77
from drivers.event_listener import AppEventListener
8+
from utils.logger import Logger, LogLevel
9+
10+
log = Logger(log_lvl=LogLevel.INFO).get_instance()
811

912

1013
@pytest.hookimpl
@@ -68,24 +71,24 @@ def driver(request):
6871
event_driver.quit()
6972

7073

71-
# def pytest_runtest_makereport(item, call):
72-
# """Capture screenshot on test failure."""
73-
# if call.excinfo is not None:
74-
# driver = item.funcargs.get("driver", None)
75-
#
76-
# if driver is not None:
77-
# screenshot_dir = "reports/screenshots"
78-
# os.makedirs(
79-
# screenshot_dir, exist_ok=True
80-
# ) # Create directory if it does not exist
81-
# screenshot_path = os.path.join(screenshot_dir, f"{item.name}.png")
82-
#
83-
# try:
84-
# driver.save_screenshot(screenshot_path)
85-
# # log.info(f"Screenshot saved to: {screenshot_path}")
86-
# except Exception as e:
87-
# pass
88-
# # log.error(f"Failed to save screenshot: {e}")
89-
# else:
90-
# pass
91-
# # log.error("Driver instance is not available for capturing screenshot.")
74+
def pytest_runtest_makereport(item, call):
75+
"""Capture screenshot on test failure."""
76+
if call.excinfo is not None:
77+
driver = item.funcargs.get("driver", None)
78+
79+
if driver is not None:
80+
screenshot_dir = "reports/screenshots"
81+
os.makedirs(
82+
screenshot_dir, exist_ok=True
83+
) # Create directory if it does not exist
84+
screenshot_path = os.path.join(screenshot_dir, f"{item.name}.png")
85+
86+
try:
87+
driver.save_screenshot(screenshot_path)
88+
log.info(f"Screenshot saved to: {screenshot_path}")
89+
except Exception as e:
90+
pass
91+
log.error(f"Failed to save screenshot: {e}")
92+
else:
93+
pass
94+
log.error("Driver instance is not available for capturing screenshot.")

src/drivers/driver_factory.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from config import settings
66
from drivers.android_driver import AndroidCaps
77
from drivers.ios_driver import IOSCaps
8+
from utils.logger import Logger, LogLevel
9+
10+
log = Logger(log_lvl=LogLevel.INFO).get_instance()
811

912

1013
class Driver:
@@ -18,12 +21,15 @@ def get_driver(platform: str):
1821
)
1922

2023
if not caps:
24+
log.info(f"Capabilities not found for platform ❌: {platform}")
2125
raise ValueError(f"Capabilities not found for platform ❌: {platform}")
2226

2327
if platform.lower() == "android":
2428
options = UiAutomator2Options().load_capabilities(caps)
29+
log.info(f"Capabilities: {options}")
2530
else:
2631
options = XCUITestOptions().load_capabilities(caps)
32+
log.info(f"Capabilities: {options}")
2733

2834
driver = webdriver.Remote(settings.APPIUM_SERVER, options=options)
2935
return driver

src/drivers/event_listener.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,28 @@
22

33
from selenium.webdriver.support.abstract_event_listener import AbstractEventListener
44

5-
# TODO make logger
6-
logger = logging.getLogger(__name__)
7-
logger.setLevel(logging.INFO)
5+
from utils.logger import Logger, LogLevel
6+
7+
log = Logger(log_lvl=LogLevel.INFO).get_instance()
88

99

1010
class AppEventListener(AbstractEventListener):
1111
"""Custom Event Listener for Appium WebDriver."""
1212

13-
def before_find(self, by, value, driver):
14-
logger.info(f"Looking for element: {by} -> {value}")
13+
# def before_find(self, by, value, driver):
14+
# logger.info(f"Looking for element: {by} -> {value}")
1515

1616
def after_find(self, by, value, driver):
17-
logger.info(f"Found element: {by} -> {value}")
17+
log.info(f"Found element: {by} -> {value}")
1818

19-
def before_click(self, element, driver):
20-
logger.info(f"Before clicking: {element}")
19+
# def before_click(self, element, driver):
20+
# logger.info(f"Before clicking: {element}")
2121

2222
def after_click(self, element, driver):
23-
logger.info(f"Clicked on: {element}")
24-
25-
def before_quit(self, driver):
26-
logger.info("Driver is about to quit.")
23+
log.info(f"Clicked on: {element}")
2724

2825
def after_quit(self, driver):
29-
logger.info("Driver has quit.")
26+
log.info("Driver has quit.")
3027

3128
def on_exception(self, exception, driver) -> None:
32-
logger.info(f"On exception")
29+
log.info(f"On exception")

src/drivers/ios_driver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def get_caps():
1010
caps = settings.iOS.to_dict()
1111

1212
if not caps:
13-
raise ValueError("❌ ANDROID capabilities not found in settings.yaml")
13+
raise ValueError("❌ iOS capabilities not found in settings.yaml")
1414

1515
caps["app"] = str(Path(__file__).resolve().parents[2] / "data/apps/demo.ipa")
1616

src/locators/locators.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ class views_menu:
1313
ANIMATION_LINK = (AppiumBy.ACCESSIBILITY_ID, "Animation")
1414
GALLERY_LINK = (AppiumBy.ACCESSIBILITY_ID, "Gallery")
1515
IMAGE_BUTTON = (AppiumBy.ACCESSIBILITY_ID, "ImageButton")
16+
TABS_LINK = (AppiumBy.ACCESSIBILITY_ID, "Tabs")
1617

17-
class views_fields:
18-
HINT_INPUT = (AppiumBy.ACCESSIBILITY_ID, "hint")
18+
class text_fields:
19+
HINT_INPUT = (AppiumBy.ID, "io.appium.android.apis:id/edit")
20+
21+
class tabs_fields:
22+
SCROLLABLE_LINK = (AppiumBy.ACCESSIBILITY_ID, "5. Scrollable")
23+
SCROLLABLE_TAB = (
24+
AppiumBy.XPATH,
25+
'//android.widget.TextView[@resource-id="android:id/title" and @text="TAB 2"]',
26+
)

src/screens/base_screen.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import time
22
from typing import Tuple, Literal
33

4+
from selenium.webdriver import ActionChains
5+
46
from screens.element_interactor import ElementInteractor
7+
from utils.logger import log
58

69

710
Locator = Tuple[str, str]
@@ -14,6 +17,7 @@ def __init__(self, driver):
1417
super().__init__(driver)
1518

1619
def click(self, locator: Locator, condition: Condition = "clickable"):
20+
"""Click on element"""
1721
element = self.element(locator, condition=condition)
1822
element.click()
1923

@@ -36,27 +40,61 @@ def tap(self, locator: Locator, duration: float = 500, **kwargs):
3640

3741
def swipe(
3842
self,
39-
relative_start_x: float,
40-
relative_start_y: float,
41-
relative_end_x: float,
42-
relative_end_y: float,
43+
start_ratio: Tuple[float, float],
44+
end_ratio: Tuple[float, float],
4345
duration_ms: int = 200,
4446
) -> None:
47+
"""Performs a swipe gesture based on screen size ratios.
48+
49+
:param start_ratio: (x, y) tuple for the starting position (0-1 range)
50+
:param end_ratio: (x, y) tuple for the ending position (0-1 range)
51+
:param duration_ms: Swipe duration in milliseconds (default: 200ms)
52+
Usage:
53+
Swipe left self.swipe((0.9, 0.5), (0.1, 0.5))
54+
55+
"""
4556
size = self.get_screen_size()
46-
width = size["width"]
47-
height = size["height"]
48-
start_x = int(width * relative_start_x)
49-
start_y = int(height * relative_start_y)
50-
end_x = int(width * relative_end_x)
51-
end_y = int(height * relative_end_y)
52-
self.driver.swipe(
53-
start_x=start_x,
54-
start_y=start_y,
55-
end_x=end_x,
56-
end_y=end_y,
57-
duration_ms=duration_ms,
57+
start_x, start_y = (
58+
int(size["width"] * start_ratio[0]),
59+
int(size["height"] * start_ratio[1]),
60+
)
61+
end_x, end_y = (
62+
int(size["width"] * end_ratio[0]),
63+
int(size["height"] * end_ratio[1]),
5864
)
5965

66+
self.driver.swipe(start_x, start_y, end_x, end_y, duration=duration_ms)
67+
68+
def swipe_to_delete(
69+
self,
70+
locator: Locator,
71+
direction: Literal["left", "right"],
72+
duration_ms: int = 500,
73+
start_ratio: float = 0.8,
74+
end_ratio: float = 0.2,
75+
):
76+
"""Swipes an element left or right to trigger a delete action.
77+
78+
:param locator: The locator of the element to swipe.
79+
:param direction: "left" or "right" to define the swipe direction.
80+
:param duration_ms: Duration of the swipe in milliseconds.
81+
:param start_ratio: Start position as a percentage of element width.
82+
:param end_ratio: End position as a percentage of element width.
83+
"""
84+
element = self.element(locator)
85+
location = element.location
86+
size = element.size
87+
88+
start_x = location["x"] + size["width"] * (
89+
start_ratio if direction == "left" else (1 - start_ratio)
90+
)
91+
end_x = location["x"] + size["width"] * (
92+
end_ratio if direction == "left" else (1 - end_ratio)
93+
)
94+
start_y = location["y"] + size["height"] // 2
95+
96+
self.driver.swipe(start_x, start_y, end_x, start_y, duration_ms)
97+
6098
def scroll(
6199
self,
62100
directions: Direction = "down",
@@ -121,21 +159,18 @@ def scroll_until_element_visible(
121159

122160
def type(self, locator: Locator, text: str):
123161
element = self.element(locator)
162+
element.clear()
124163
element.send_keys(text)
125164

126165
def double_tap(
127166
self, locator: Locator, condition: Condition = "clickable", **kwargs
128167
):
129168
"""Double taps on an element."""
130169
try:
131-
element = self.element(locator, condition=condition, **kwargs)
132-
# TODO
170+
self.double_tap_actions(locator, condition=condition, **kwargs)
133171
except Exception as e:
134172
print(f"Error during double tap action: {e}")
135173

136-
def long_press(self):
137-
pass
138-
139174
@staticmethod
140175
def sleep(kwargs):
141176
try:

src/screens/element_interactor.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717

1818
class WaitType(Enum):
19-
DEFAULT = 30
19+
DEFAULT = 15
20+
SHORTEST = 2
2021
SHORT = 5
2122
LONG = 60
2223
FLUENT = 10
@@ -128,7 +129,7 @@ def is_exist(
128129
expected: bool = True,
129130
n: int = 3,
130131
condition: Condition = "visible",
131-
wait_type: Optional[WaitType] = WaitType.SHORT,
132+
wait_type: Optional[WaitType] = WaitType.SHORTEST,
132133
retry_delay: float = 0.5,
133134
) -> bool:
134135
"""
@@ -203,3 +204,33 @@ def scroll_by_coordinates(
203204
actions.w3c_actions.pointer_action.release()
204205

205206
actions.perform()
207+
208+
def double_tap_actions(
209+
self,
210+
locator,
211+
condition: Condition = "clickable",
212+
index: Optional[int] = None,
213+
n: int = 2,
214+
):
215+
"""
216+
Performs a double tap using ActionChains.
217+
218+
- Waits for the element(s) to be visible
219+
- Works for both single and multiple elements (use index for multiple)
220+
221+
:param condition:
222+
:param locator: Tuple (By, value)
223+
:param index: Index of element in case of multiple elements
224+
:param n: Number of attempts to locate element
225+
"""
226+
elements = self.elements(locator, condition=condition, n=n)
227+
228+
if not elements:
229+
raise NoSuchElementException(
230+
f"Could not locate element with value: {locator}"
231+
)
232+
233+
element = elements[index] if index is not None else elements[0]
234+
235+
actions = ActionChains(self.driver)
236+
actions.double_click(element).perform()

src/screens/main_screen/main_screen.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from locators.locators import Locators
44
from screens.base_screen import Screen
5+
from utils.logger import log
56

67

78
class MainScreen(Screen):
@@ -10,24 +11,56 @@ def __init__(self, driver):
1011
self.locators = Locators()
1112

1213
def click_on_text_link(self):
14+
"""Click on text link"""
1315
self.click(locator=self.locators.main_menu.TEXT_LINK)
1416

1517
def tap_on_text_link(self):
18+
"""Tap on text link"""
1619
self.tap(locator=self.locators.main_menu.TEXT_LINK)
1720

1821
def scroll_view_by_coordinates(self, direction: Literal["down", "up"] = "down"):
22+
"""Scroll by coordinates"""
1923
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
2024
self.scroll(directions=direction)
2125

2226
def scroll_to_image_button(self):
27+
"""Scroll to image button"""
2328
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
2429
self.scroll_to_element(
2530
from_el=self.locators.views_menu.ANIMATION_LINK,
2631
destination_el=self.locators.views_menu.IMAGE_BUTTON,
2732
)
2833

2934
def scroll_until_text_field_visible(self):
35+
"""Scroll until element visible"""
3036
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
3137
self.scroll_until_element_visible(
3238
destination_el=self.locators.views_menu.TEXT_FIELDS
3339
)
40+
41+
def swipe_tab(self):
42+
"""Move to Scrollable tab and swipe left"""
43+
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
44+
self.scroll_until_element_visible(
45+
destination_el=self.locators.views_menu.TABS_LINK
46+
)
47+
self.tap(locator=self.locators.views_menu.TABS_LINK)
48+
self.tap(locator=self.locators.views_menu.tabs_fields.SCROLLABLE_LINK)
49+
self.swipe_to_delete(
50+
locator=self.locators.views_menu.tabs_fields.SCROLLABLE_TAB,
51+
direction="left",
52+
)
53+
54+
def type_text(self, text):
55+
"""Type text to field with HINT"""
56+
self.tap(locator=self.locators.main_menu.VIEWS_LINK)
57+
self.scroll_until_element_visible(
58+
destination_el=self.locators.views_menu.TEXT_FIELDS
59+
)
60+
self.tap(locator=self.locators.views_menu.TEXT_FIELDS)
61+
self.click(locator=self.locators.views_menu.text_fields.HINT_INPUT)
62+
self.type(locator=self.locators.views_menu.text_fields.HINT_INPUT, text=text)
63+
64+
def double_tap_on_views_link(self):
65+
"""Double tap"""
66+
self.double_tap(locator=self.locators.main_menu.VIEWS_LINK)

0 commit comments

Comments
 (0)