22import platform
33import json
44from functools import lru_cache
5+ from time import sleep
56import requests
67
78from selenium .webdriver import __version__ as SELENIUM_VERSION
9+ from selenium .common .exceptions import TimeoutException
10+ from selenium .webdriver .support .ui import WebDriverWait
811from percy .version import __version__ as SDK_VERSION
912from percy .driver_metadata import DriverMetaData
1013
1518# Maybe get the CLI API address from the environment
1619PERCY_CLI_API = os .environ .get ('PERCY_CLI_API' ) or 'http://localhost:5338'
1720PERCY_DEBUG = os .environ .get ('PERCY_LOGLEVEL' ) == 'debug'
21+ RESONSIVE_CAPTURE_SLEEP_TIME = os .environ .get ('RESONSIVE_CAPTURE_SLEEP_TIME' )
1822
1923# for logging
2024LABEL = '[\u001b [35m' + ('percy:python' if PERCY_DEBUG else 'percy' ) + '\u001b [39m]'
25+ CDP_SUPPORT_SELENIUM = (str (SELENIUM_VERSION )[0 ].isdigit () and int (
26+ str (SELENIUM_VERSION )[0 ]) >= 4 ) if SELENIUM_VERSION else False
27+
28+ def log (message , lvl = 'info' ):
29+ message = f'{ LABEL } { message } '
30+ try :
31+ requests .post (f'{ PERCY_CLI_API } /percy/log' ,
32+ json = {'message' : message , 'level' : lvl }, timeout = 1 )
33+ except Exception as e :
34+ if PERCY_DEBUG : print (f'Sending log to CLI Failed { e } ' )
35+ finally :
36+ # Only log if lvl is 'debug' and PERCY_DEBUG is True
37+ if lvl != 'debug' or PERCY_DEBUG :
38+ print (message )
2139
2240# Check if Percy is enabled, caching the result so it is only checked once
2341@lru_cache (maxsize = None )
@@ -27,6 +45,8 @@ def is_percy_enabled():
2745 response .raise_for_status ()
2846 data = response .json ()
2947 session_type = data .get ('type' , None )
48+ widths = data .get ('widths' , {})
49+ config = data .get ('config' , {})
3050
3151 if not data ['success' ]: raise Exception (data ['error' ])
3252 version = response .headers .get ('x-percy-core-version' )
@@ -42,7 +62,11 @@ def is_percy_enabled():
4262 print (f'{ LABEL } Unsupported Percy CLI version, { version } ' )
4363 return False
4464
45- return session_type
65+ return {
66+ 'session_type' : session_type ,
67+ 'config' : config ,
68+ 'widths' : widths
69+ }
4670 except Exception as e :
4771 print (f'{ LABEL } Percy is not running, disabling snapshots' )
4872 if PERCY_DEBUG : print (f'{ LABEL } { e } ' )
@@ -55,22 +79,94 @@ def fetch_percy_dom():
5579 response .raise_for_status ()
5680 return response .text
5781
82+ def get_serialized_dom (driver , cookies , ** kwargs ):
83+ dom_snapshot = driver .execute_script (f'return PercyDOM.serialize({ json .dumps (kwargs )} )' )
84+ dom_snapshot ['cookies' ] = cookies
85+ return dom_snapshot
86+
87+ def get_widths_for_multi_dom (eligible_widths , ** kwargs ):
88+ user_passed_widths = kwargs .get ('widths' , [])
89+ width = kwargs .get ('width' )
90+ if width : user_passed_widths = [width ]
91+
92+ # Deep copy mobile widths otherwise it will get overridden
93+ allWidths = eligible_widths .get ('mobile' , [])[:]
94+ if len (user_passed_widths ) != 0 :
95+ allWidths .extend (user_passed_widths )
96+ else :
97+ allWidths .extend (eligible_widths .get ('config' , []))
98+ return list (set (allWidths ))
99+
100+ def change_window_dimension_and_wait (driver , width , height , resizeCount ):
101+ try :
102+ if CDP_SUPPORT_SELENIUM and driver .capabilities ['browserName' ] == 'chrome' :
103+ driver .execute_cdp_cmd ('Emulation.setDeviceMetricsOverride' , { 'height' : height ,
104+ 'width' : width , 'deviceScaleFactor' : 1 , 'mobile' : False })
105+ else :
106+ driver .set_window_size (width , height )
107+ except Exception as e :
108+ log (f'Resizing using cdp failed falling back driver for width { width } { e } ' , 'debug' )
109+ driver .set_window_size (width , height )
110+
111+ try :
112+ WebDriverWait (driver , 1 ).until (
113+ lambda driver : driver .execute_script ("return window.resizeCount" ) == resizeCount
114+ )
115+ except TimeoutException :
116+ log (f"Timed out waiting for window resize event for width { width } " , 'debug' )
117+
118+
119+ def capture_responsive_dom (driver , eligible_widths , cookies , ** kwargs ):
120+ widths = get_widths_for_multi_dom (eligible_widths , ** kwargs )
121+ dom_snapshots = []
122+ window_size = driver .get_window_size ()
123+ current_width , current_height = window_size ['width' ], window_size ['height' ]
124+ last_window_width = current_width
125+ resize_count = 0
126+ driver .execute_script ("PercyDOM.waitForResize()" )
127+
128+ for width in widths :
129+ if last_window_width != width :
130+ resize_count += 1
131+ change_window_dimension_and_wait (driver , width , current_height , resize_count )
132+ last_window_width = width
133+
134+ if RESONSIVE_CAPTURE_SLEEP_TIME : sleep (int (RESONSIVE_CAPTURE_SLEEP_TIME ))
135+ dom_snapshot = get_serialized_dom (driver , cookies , ** kwargs )
136+ dom_snapshot ['width' ] = width
137+ dom_snapshots .append (dom_snapshot )
138+
139+ change_window_dimension_and_wait (driver , current_width , current_height , resize_count + 1 )
140+ return dom_snapshots
141+
142+ def is_responsive_snapshot_capture (config , ** kwargs ):
143+ # Don't run resposive snapshot capture when defer uploads is enabled
144+ if 'percy' in config and config ['percy' ].get ('deferUploads' , False ): return False
145+
146+ return kwargs .get ('responsive_snapshot_capture' , False ) or kwargs .get (
147+ 'responsiveSnapshotCapture' , False ) or (
148+ 'snapshot' in config and config ['snapshot' ].get ('responsiveSnapshotCapture' ))
149+
58150# Take a DOM snapshot and post it to the snapshot endpoint
59151def percy_snapshot (driver , name , ** kwargs ):
60- session_type = is_percy_enabled ()
61- if session_type is False : return None # Since session_type can be None for old CLI version
62- if session_type == "automate" : raise Exception ("Invalid function call - " \
152+ data = is_percy_enabled ()
153+ if not data : return None
154+
155+ if data ['session_type' ] == "automate" : raise Exception ("Invalid function call - " \
63156 "percy_snapshot(). Please use percy_screenshot() function while using Percy with Automate. " \
64157 "For more information on usage of PercyScreenshot, " \
65158 "refer https://www.browserstack.com/docs/percy/integrate/functional-and-visual" )
66159
67-
68160 try :
69161 # Inject the DOM serialization script
70162 driver .execute_script (fetch_percy_dom ())
163+ cookies = driver .get_cookies ()
71164
72165 # Serialize and capture the DOM
73- dom_snapshot = driver .execute_script (f'return PercyDOM.serialize({ json .dumps (kwargs )} )' )
166+ if is_responsive_snapshot_capture (data ['config' ], ** kwargs ):
167+ dom_snapshot = capture_responsive_dom (driver , data ['widths' ], cookies , ** kwargs )
168+ else :
169+ dom_snapshot = get_serialized_dom (driver , cookies , ** kwargs )
74170
75171 # Post the DOM to the snapshot endpoint with snapshot options and other info
76172 response = requests .post (f'{ PERCY_CLI_API } /percy/snapshot' , json = {** kwargs , ** {
@@ -88,15 +184,16 @@ def percy_snapshot(driver, name, **kwargs):
88184 if not data ['success' ]: raise Exception (data ['error' ])
89185 return data .get ("data" , None )
90186 except Exception as e :
91- print (f'{ LABEL } Could not take DOM snapshot "{ name } "' )
92- print (f'{ LABEL } { e } ' )
187+ log (f'Could not take DOM snapshot "{ name } "' )
188+ log (f'{ e } ' )
93189 return None
94190
95191# Take screenshot on driver
96192def percy_automate_screenshot (driver , name , options = None , ** kwargs ):
97- session_type = is_percy_enabled ()
98- if session_type is False : return None # Since session_type can be None for old CLI version
99- if session_type != "automate" : raise Exception ("Invalid function call - " \
193+ data = is_percy_enabled ()
194+ if not data : return None
195+
196+ if data ['session_type' ] != "automate" : raise Exception ("Invalid function call - " \
100197 "percy_screenshot(). Please use percy_snapshot() function for taking screenshot. " \
101198 "percy_screenshot() should be used only while using Percy with Automate. " \
102199 "For more information on usage of percy_snapshot(), " \
@@ -145,8 +242,8 @@ def percy_automate_screenshot(driver, name, options = None, **kwargs):
145242 if not data ['success' ]: raise Exception (data ['error' ])
146243 return data .get ("data" , None )
147244 except Exception as e :
148- print (f'{ LABEL } Could not take Screenshot "{ name } "' )
149- print (f'{ LABEL } { e } ' )
245+ log (f'Could not take Screenshot "{ name } "' )
246+ log (f'{ e } ' )
150247 return None
151248
152249def get_element_ids (elements ):
0 commit comments