Skip to content

Commit 7c30459

Browse files
authored
Merge pull request #41 from LambdaTest/stage
Stage
2 parents 6a74dc4 + 55dc4a0 commit 7c30459

File tree

12 files changed

+1192
-5
lines changed

12 files changed

+1192
-5
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from lambdatest_selenium_driver.version import __version__
22
from lambdatest_selenium_driver.smartui import smartui_snapshot
3+
from lambdatest_selenium_driver.smartui_app_snapshot import SmartUIAppSnapshot
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
import os
2+
import time
3+
import tempfile
4+
from typing import List, Dict, Any, Optional
5+
from selenium.webdriver.remote.webdriver import WebDriver
6+
from selenium.webdriver.remote.webelement import WebElement
7+
from selenium.common.exceptions import WebDriverException
8+
from lambdatest_sdk_utils.logger import get_logger
9+
10+
logger = get_logger('lambdatest-selenium-driver')
11+
12+
# Constants
13+
DEFAULT_PAGE_COUNT = 20
14+
MAX_PAGE_COUNT = 30
15+
SCROLL_DELAY_MS = 0.2
16+
WEB_SCROLL_PAUSE_MS = 1.0
17+
IOS_SCROLL_DURATION_MS = 1500
18+
ANDROID_SCROLL_SPEED = 1500
19+
PAGE_SOURCE_CHECK_DELAY_MS = 0.1
20+
21+
# Scroll percentages
22+
ANDROID_SCROLL_END_PERCENT = 0.3
23+
ANDROID_SCROLL_HEIGHT_PERCENT = 0.35
24+
IOS_SCROLL_HEIGHT_PERCENT = 0.3
25+
IOS_START_Y_PERCENT = 0.7
26+
IOS_END_Y_PERCENT = 0.4
27+
WEB_SCROLL_HEIGHT_PERCENT = 0.4
28+
29+
30+
class FullPageScreenshotUtil:
31+
"""Utility class for capturing full-page screenshots with scrolling."""
32+
33+
def __init__(self, driver: WebDriver, save_directory_name: str,
34+
test_type: str = "app", precise_scroll: bool = False):
35+
"""
36+
Initialize the full page screenshot utility.
37+
38+
Args:
39+
driver: Selenium WebDriver instance
40+
save_directory_name: Directory name to save screenshots
41+
test_type: Test type ("app" or "web")
42+
precise_scroll: Whether to use precise scrolling
43+
"""
44+
self.driver = driver
45+
self.save_directory_name = save_directory_name
46+
self.test_type = test_type
47+
self.precise_scroll = precise_scroll
48+
self.platform = self._detect_platform()
49+
self.device_name = self._detect_device_name()
50+
self.prev_page_source = ""
51+
self.default_page_count = DEFAULT_PAGE_COUNT
52+
53+
self._create_directory_if_needed()
54+
55+
def _create_directory_if_needed(self):
56+
"""Create directory for saving screenshots if it doesn't exist."""
57+
if not os.path.exists(self.save_directory_name):
58+
os.makedirs(self.save_directory_name)
59+
logger.info(f"Created directory: {self.save_directory_name}")
60+
61+
def capture_full_page_screenshot(self, page_count: int) -> Dict[str, Any]:
62+
"""
63+
Capture full-page screenshot by scrolling and taking multiple screenshots.
64+
65+
Args:
66+
page_count: Maximum number of pages to capture (0 means use default)
67+
68+
Returns:
69+
Dictionary containing list of screenshot file paths
70+
"""
71+
self._initialize_page_count(page_count)
72+
73+
screenshot_files = []
74+
chunk_count = 0
75+
is_last_scroll = False
76+
77+
while not is_last_scroll and chunk_count < self.default_page_count:
78+
screenshot_file = self._capture_and_save_screenshot(chunk_count)
79+
screenshot_files.append(screenshot_file)
80+
81+
chunk_count += 1
82+
self._scroll_down()
83+
is_last_scroll = self._has_reached_bottom()
84+
85+
return {
86+
"screenshots": screenshot_files
87+
}
88+
89+
def _initialize_page_count(self, page_count: int):
90+
"""Initialize page count with validation."""
91+
if page_count == 0:
92+
self.default_page_count = DEFAULT_PAGE_COUNT
93+
elif page_count > MAX_PAGE_COUNT:
94+
logger.warning(f"Page count {page_count} exceeds maximum {MAX_PAGE_COUNT}, "
95+
f"using {MAX_PAGE_COUNT}")
96+
self.default_page_count = MAX_PAGE_COUNT
97+
else:
98+
self.default_page_count = page_count
99+
100+
logger.info(f"Page count set to: {self.default_page_count}")
101+
102+
def _capture_and_save_screenshot(self, index: int) -> str:
103+
"""Capture and save a screenshot."""
104+
screenshot_file_path = os.path.join(
105+
self.save_directory_name,
106+
f"{self.save_directory_name}_{index}.png"
107+
)
108+
109+
try:
110+
# Take screenshot
111+
screenshot = self.driver.get_screenshot_as_png()
112+
113+
# Save to file
114+
with open(screenshot_file_path, 'wb') as f:
115+
f.write(screenshot)
116+
117+
logger.info(f"Saved screenshot: {screenshot_file_path}")
118+
return screenshot_file_path
119+
except Exception as e:
120+
logger.error(f"Error saving screenshot: {e}")
121+
raise
122+
123+
def _scroll_down(self) -> int:
124+
"""Scroll down the page."""
125+
try:
126+
time.sleep(SCROLL_DELAY_MS)
127+
if self.test_type.lower() == "app":
128+
if self.platform == "ios":
129+
return self._scroll_ios()
130+
else:
131+
return self._scroll_android()
132+
else:
133+
return self._scroll_web()
134+
except Exception as e:
135+
logger.error(f"Error in scroll_down: {e}")
136+
return 0
137+
138+
def _scroll_ios(self) -> int:
139+
"""Scroll on iOS platform."""
140+
try:
141+
# Get window size
142+
rect = self.driver.get_window_rect()
143+
scroll_height = int(rect['height'] * IOS_SCROLL_HEIGHT_PERCENT)
144+
145+
# Try primary scroll method using JavaScript
146+
if self._try_touch_swipe_ios():
147+
time.sleep(0.5)
148+
return scroll_height
149+
150+
# Fallback methods
151+
if self._try_drag_from_to_ios(rect):
152+
time.sleep(0.5)
153+
return scroll_height
154+
155+
if self._try_javascript_scroll_ios(scroll_height):
156+
time.sleep(0.5)
157+
return scroll_height
158+
159+
logger.warning("All iOS scroll methods failed")
160+
return 0
161+
except Exception as e:
162+
logger.error(f"iOS scroll failed: {e}")
163+
return 0
164+
165+
def _scroll_android(self) -> int:
166+
"""Scroll on Android platform."""
167+
try:
168+
# Get window size
169+
rect = self.driver.get_window_rect()
170+
scroll_height = int(rect['height'] * ANDROID_SCROLL_HEIGHT_PERCENT)
171+
172+
# Try primary scroll method
173+
if self._try_touch_swipe_android():
174+
time.sleep(0.2)
175+
return scroll_height
176+
177+
# Fallback methods
178+
if self._try_drag_from_to_android(rect):
179+
time.sleep(0.2)
180+
return scroll_height
181+
182+
if self._try_javascript_scroll_android(scroll_height):
183+
time.sleep(0.2)
184+
return scroll_height
185+
186+
logger.warning("All Android scroll methods failed")
187+
return 0
188+
except Exception as e:
189+
logger.error(f"Android scroll failed: {e}")
190+
return 0
191+
192+
def _scroll_web(self) -> int:
193+
"""Scroll on web platform."""
194+
try:
195+
# Get window size
196+
rect = self.driver.get_window_rect()
197+
scroll_height = int(rect['height'] * WEB_SCROLL_HEIGHT_PERCENT)
198+
199+
self.driver.execute_script(f"window.scrollBy(0, {scroll_height});")
200+
time.sleep(WEB_SCROLL_PAUSE_MS)
201+
return scroll_height
202+
except Exception as e:
203+
logger.error(f"Web JavaScript scroll failed: {e}")
204+
return 0
205+
206+
def _try_touch_swipe_ios(self) -> bool:
207+
"""Try iOS touch swipe method."""
208+
try:
209+
params = {
210+
"start": "50%,70%",
211+
"end": "50%,40%",
212+
"duration": "2"
213+
}
214+
self.driver.execute_script("mobile:touch:swipe", params)
215+
return True
216+
except Exception as e:
217+
logger.debug(f"iOS touch:swipe failed: {e}")
218+
return False
219+
220+
def _try_drag_from_to_ios(self, rect: Dict[str, int]) -> bool:
221+
"""Try iOS drag from to method."""
222+
try:
223+
center_x = rect["width"] // 2
224+
start_y = int(rect["height"] * IOS_START_Y_PERCENT)
225+
end_y = int(rect["height"] * IOS_END_Y_PERCENT)
226+
227+
swipe_obj = {
228+
"fromX": center_x,
229+
"fromY": start_y,
230+
"toX": center_x,
231+
"toY": end_y,
232+
"duration": 2.0
233+
}
234+
self.driver.execute_script("mobile:dragFromToForDuration", swipe_obj)
235+
return True
236+
except Exception as e:
237+
logger.debug(f"iOS dragFromToForDuration failed: {e}")
238+
return False
239+
240+
def _try_javascript_scroll_ios(self, scroll_height: int) -> bool:
241+
"""Try JavaScript scroll for iOS."""
242+
try:
243+
self.driver.execute_script(
244+
f"window.scrollTo({{top: window.pageYOffset + {scroll_height}, behavior: 'smooth'}});"
245+
)
246+
return True
247+
except Exception as e:
248+
logger.debug(f"iOS JavaScript scroll failed: {e}")
249+
return False
250+
251+
def _try_touch_swipe_android(self) -> bool:
252+
"""Try Android touch swipe method."""
253+
try:
254+
params = {
255+
"start": "50%,70%",
256+
"end": "50%,30%",
257+
"duration": "2"
258+
}
259+
self.driver.execute_script("mobile:touch:swipe", params)
260+
return True
261+
except Exception as e:
262+
logger.debug(f"Android touch:swipe failed: {e}")
263+
return False
264+
265+
def _try_drag_from_to_android(self, rect: Dict[str, int]) -> bool:
266+
"""Try Android drag from to method."""
267+
try:
268+
center_x = rect["width"] // 2
269+
start_y = int(rect["height"] * ANDROID_SCROLL_END_PERCENT)
270+
end_y = int(rect["height"] * ANDROID_SCROLL_HEIGHT_PERCENT)
271+
272+
swipe_obj = {
273+
"fromX": center_x,
274+
"fromY": start_y,
275+
"toX": center_x,
276+
"toY": end_y,
277+
"duration": 2.0
278+
}
279+
self.driver.execute_script("mobile:dragFromToForDuration", swipe_obj)
280+
return True
281+
except Exception as e:
282+
logger.debug(f"Android dragFromToForDuration failed: {e}")
283+
return False
284+
285+
def _try_javascript_scroll_android(self, scroll_height: int) -> bool:
286+
"""Try JavaScript scroll for Android."""
287+
try:
288+
self.driver.execute_script(
289+
f"window.scrollTo({{top: window.pageYOffset + {scroll_height}, behavior: 'smooth'}});"
290+
)
291+
return True
292+
except Exception as e:
293+
logger.debug(f"Android JavaScript scroll failed: {e}")
294+
return False
295+
296+
def _has_reached_bottom(self) -> bool:
297+
"""Check if the bottom of the page has been reached."""
298+
try:
299+
time.sleep(PAGE_SOURCE_CHECK_DELAY_MS)
300+
301+
if self.test_type.lower() == "web":
302+
return self._has_reached_bottom_web()
303+
else:
304+
return self._has_reached_bottom_mobile()
305+
except Exception as e:
306+
logger.warning(f"Error checking if reached bottom: {e}")
307+
return True
308+
309+
def _has_reached_bottom_web(self) -> bool:
310+
"""Check if bottom reached for web."""
311+
try:
312+
current_scroll_y = self.driver.execute_script(
313+
"return window.pageYOffset || document.documentElement.scrollTop || "
314+
"document.body.scrollTop || 0;"
315+
)
316+
317+
page_height = self.driver.execute_script(
318+
"return Math.max("
319+
"document.body.scrollHeight, "
320+
"document.body.offsetHeight, "
321+
"document.documentElement.clientHeight, "
322+
"document.documentElement.scrollHeight, "
323+
"document.documentElement.offsetHeight);"
324+
)
325+
326+
viewport_height = self.driver.execute_script(
327+
"return window.innerHeight || document.documentElement.clientHeight || "
328+
"document.body.clientHeight;"
329+
)
330+
331+
is_at_bottom = (current_scroll_y + viewport_height) >= page_height
332+
return is_at_bottom
333+
except Exception as e:
334+
logger.warning(f"Error checking web bottom: {e}")
335+
return True
336+
337+
def _has_reached_bottom_mobile(self) -> bool:
338+
"""Check if bottom reached for mobile by comparing page source."""
339+
try:
340+
current_page_source = self.driver.page_source
341+
342+
if self.prev_page_source == current_page_source:
343+
return True
344+
345+
self.prev_page_source = current_page_source
346+
return False
347+
except Exception as e:
348+
logger.warning(f"Error checking mobile bottom: {e}")
349+
return True
350+
351+
def _detect_platform(self) -> str:
352+
"""Detect the platform (ios, android, or web)."""
353+
try:
354+
if hasattr(self.driver, 'capabilities'):
355+
caps = self.driver.capabilities
356+
platform_name = caps.get("platformName", "").lower() or caps.get("platform", "").lower()
357+
358+
if "ios" in platform_name:
359+
return "ios"
360+
elif "android" in platform_name:
361+
return "android"
362+
else:
363+
return "web"
364+
else:
365+
return "web"
366+
except Exception as e:
367+
logger.warning(f"Failed to detect platform: {e}")
368+
return "web"
369+
370+
def _detect_device_name(self) -> str:
371+
"""Detect the device name from capabilities."""
372+
try:
373+
if hasattr(self.driver, 'capabilities'):
374+
caps = self.driver.capabilities
375+
device_keys = ["deviceName", "device", "deviceModel", "deviceType"]
376+
377+
for key in device_keys:
378+
desired = caps.get("desired", {})
379+
if key in desired and desired[key]:
380+
return str(desired[key])
381+
for key in device_keys:
382+
if key in caps and caps[key]:
383+
return str(caps[key])
384+
385+
logger.info("No device name capability found, using platform as device identifier")
386+
return self.platform
387+
else:
388+
return self.platform
389+
except Exception as e:
390+
logger.warning(f"Failed to detect device name: {e}")
391+
return self.platform
392+

0 commit comments

Comments
 (0)