Skip to content

Commit 42e056f

Browse files
Added support for responsive snapshot capture (#127)
* Added support for reponsive snapshot capture * Lint fixes * Address some comments * Lint fixes * Addressed all comments * Refactor code added try catch and additional conditions * Lint fix * update logging logic * Modified logic to get resize script from cli + config from cli * Dont run responsive capture if defer uploads is true * Lint fix
1 parent 09939de commit 42e056f

File tree

2 files changed

+236
-18
lines changed

2 files changed

+236
-18
lines changed

percy/snapshot.py

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
import platform
33
import json
44
from functools import lru_cache
5+
from time import sleep
56
import requests
67

78
from selenium.webdriver import __version__ as SELENIUM_VERSION
9+
from selenium.common.exceptions import TimeoutException
10+
from selenium.webdriver.support.ui import WebDriverWait
811
from percy.version import __version__ as SDK_VERSION
912
from percy.driver_metadata import DriverMetaData
1013

@@ -15,9 +18,24 @@
1518
# Maybe get the CLI API address from the environment
1619
PERCY_CLI_API = os.environ.get('PERCY_CLI_API') or 'http://localhost:5338'
1720
PERCY_DEBUG = os.environ.get('PERCY_LOGLEVEL') == 'debug'
21+
RESONSIVE_CAPTURE_SLEEP_TIME = os.environ.get('RESONSIVE_CAPTURE_SLEEP_TIME')
1822

1923
# for logging
2024
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)
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
59151
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 - "\
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
96192
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 - "\
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

152249
def get_element_ids(elements):

0 commit comments

Comments
 (0)