2
2
import platform
3
3
import json
4
4
from functools import lru_cache
5
+ from time import sleep
5
6
import requests
6
7
7
8
from selenium .webdriver import __version__ as SELENIUM_VERSION
9
+ from selenium .common .exceptions import TimeoutException
10
+ from selenium .webdriver .support .ui import WebDriverWait
8
11
from percy .version import __version__ as SDK_VERSION
9
12
from percy .driver_metadata import DriverMetaData
10
13
15
18
# Maybe get the CLI API address from the environment
16
19
PERCY_CLI_API = os .environ .get ('PERCY_CLI_API' ) or 'http://localhost:5338'
17
20
PERCY_DEBUG = os .environ .get ('PERCY_LOGLEVEL' ) == 'debug'
21
+ RESONSIVE_CAPTURE_SLEEP_TIME = os .environ .get ('RESONSIVE_CAPTURE_SLEEP_TIME' )
18
22
19
23
# for logging
20
24
LABEL = '[\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 )
21
39
22
40
# Check if Percy is enabled, caching the result so it is only checked once
23
41
@lru_cache (maxsize = None )
@@ -27,6 +45,8 @@ def is_percy_enabled():
27
45
response .raise_for_status ()
28
46
data = response .json ()
29
47
session_type = data .get ('type' , None )
48
+ widths = data .get ('widths' , {})
49
+ config = data .get ('config' , {})
30
50
31
51
if not data ['success' ]: raise Exception (data ['error' ])
32
52
version = response .headers .get ('x-percy-core-version' )
@@ -42,7 +62,11 @@ def is_percy_enabled():
42
62
print (f'{ LABEL } Unsupported Percy CLI version, { version } ' )
43
63
return False
44
64
45
- return session_type
65
+ return {
66
+ 'session_type' : session_type ,
67
+ 'config' : config ,
68
+ 'widths' : widths
69
+ }
46
70
except Exception as e :
47
71
print (f'{ LABEL } Percy is not running, disabling snapshots' )
48
72
if PERCY_DEBUG : print (f'{ LABEL } { e } ' )
@@ -55,22 +79,94 @@ def fetch_percy_dom():
55
79
response .raise_for_status ()
56
80
return response .text
57
81
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
+
58
150
# Take a DOM snapshot and post it to the snapshot endpoint
59
151
def 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 - " \
63
156
"percy_snapshot(). Please use percy_screenshot() function while using Percy with Automate. " \
64
157
"For more information on usage of PercyScreenshot, " \
65
158
"refer https://www.browserstack.com/docs/percy/integrate/functional-and-visual" )
66
159
67
-
68
160
try :
69
161
# Inject the DOM serialization script
70
162
driver .execute_script (fetch_percy_dom ())
163
+ cookies = driver .get_cookies ()
71
164
72
165
# 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 )
74
170
75
171
# Post the DOM to the snapshot endpoint with snapshot options and other info
76
172
response = requests .post (f'{ PERCY_CLI_API } /percy/snapshot' , json = {** kwargs , ** {
@@ -88,15 +184,16 @@ def percy_snapshot(driver, name, **kwargs):
88
184
if not data ['success' ]: raise Exception (data ['error' ])
89
185
return data .get ("data" , None )
90
186
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 } ' )
93
189
return None
94
190
95
191
# Take screenshot on driver
96
192
def 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 - " \
100
197
"percy_screenshot(). Please use percy_snapshot() function for taking screenshot. " \
101
198
"percy_screenshot() should be used only while using Percy with Automate. " \
102
199
"For more information on usage of percy_snapshot(), " \
@@ -145,8 +242,8 @@ def percy_automate_screenshot(driver, name, options = None, **kwargs):
145
242
if not data ['success' ]: raise Exception (data ['error' ])
146
243
return data .get ("data" , None )
147
244
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 } ' )
150
247
return None
151
248
152
249
def get_element_ids (elements ):
0 commit comments