helpers.py (10025B)
1 import base64 2 import collections 3 import math 4 import sys 5 import os 6 from urllib.parse import urlparse 7 8 import webdriver 9 10 from tests.support import defaults 11 from tests.support.sync import Poll 12 13 14 def ignore_exceptions(f): 15 def inner(session, *args, **kwargs): 16 # Do not try to clean up already ended session. 17 if session.session_id is None: 18 return 19 try: 20 return f(session, *args, **kwargs) 21 except webdriver.error.WebDriverException as e: 22 print("Ignored exception %s" % e, file=sys.stderr) 23 inner.__name__ = f.__name__ 24 return inner 25 26 27 def cleanup_session(session): 28 """Clean-up the current session for a clean state.""" 29 @ignore_exceptions 30 def _dismiss_user_prompts(session): 31 """Dismiss any open user prompts in windows.""" 32 current_window = session.window_handle 33 34 for window in _windows(session): 35 session.window_handle = window 36 try: 37 session.alert.dismiss() 38 except webdriver.NoSuchAlertException: 39 pass 40 41 session.window_handle = current_window 42 43 @ignore_exceptions 44 def _ensure_valid_window(session): 45 """If current window was closed, ensure to have a valid one selected.""" 46 try: 47 session.window_handle 48 except webdriver.NoSuchWindowException: 49 handles = session.handles 50 if handles: 51 # Update only when there is at least one valid window left. 52 session.window_handle = handles[0] 53 54 @ignore_exceptions 55 def _restore_timeouts(session): 56 """Restore modified timeouts to their default values.""" 57 session.timeouts.implicit = defaults.IMPLICIT_WAIT_TIMEOUT 58 session.timeouts.page_load = defaults.PAGE_LOAD_TIMEOUT 59 session.timeouts.script = defaults.SCRIPT_TIMEOUT 60 61 @ignore_exceptions 62 def _restore_window_state(session): 63 """Reset window to an acceptable size. 64 65 This also includes bringing it out of maximized, minimized, 66 or fullscreen state. 67 """ 68 if session.capabilities.get("setWindowRect"): 69 session.window.size = defaults.WINDOW_SIZE 70 71 @ignore_exceptions 72 def _restore_windows(session): 73 """Close superfluous windows opened by the test. 74 75 It will not end the session implicitly by closing the last window. 76 """ 77 current_window = session.window_handle 78 79 for window in _windows(session, exclude=[current_window]): 80 session.window_handle = window 81 if len(session.handles) > 1: 82 session.window.close() 83 84 session.window_handle = current_window 85 86 _restore_timeouts(session) 87 _ensure_valid_window(session) 88 _dismiss_user_prompts(session) 89 _restore_windows(session) 90 _restore_window_state(session) 91 _switch_to_top_level_browsing_context(session) 92 93 94 @ignore_exceptions 95 def _switch_to_top_level_browsing_context(session): 96 """If the current browsing context selected by WebDriver is a 97 `<frame>` or an `<iframe>`, switch it back to the top-level 98 browsing context. 99 """ 100 session.switch_to_frame(None) 101 102 103 def _windows(session, exclude=None): 104 """Set of window handles, filtered by an `exclude` list if 105 provided. 106 """ 107 if exclude is None: 108 exclude = [] 109 wins = [w for w in session.handles if w not in exclude] 110 return set(wins) 111 112 113 114 115 def deep_update(source, overrides): 116 """ 117 Update a nested dictionary or similar mapping. 118 Modify ``source`` in place. 119 """ 120 for key, value in overrides.items(): 121 if isinstance(value, collections.abc.Mapping) and value: 122 source[key] = deep_update(source.get(key, {}), value) 123 elif isinstance(value, list) and isinstance(source.get(key), list) and value: 124 # Concatenate lists, ensuring all elements are kept without duplicates 125 source[key] = list(dict.fromkeys(source[key] + value)) 126 else: 127 source[key] = value 128 129 return source 130 131 132 def document_dimensions(session): 133 return tuple(session.execute_script(""" 134 const {devicePixelRatio} = window; 135 const {width, height} = document.documentElement.getBoundingClientRect(); 136 return [width * devicePixelRatio, height * devicePixelRatio]; 137 """)) 138 139 140 def center_point(element): 141 """Calculates the in-view center point of a web element.""" 142 inner_width, inner_height = element.session.execute_script( 143 "return [window.innerWidth, window.innerHeight]") 144 rect = element.rect 145 146 # calculate the intersection of the rect that is inside the viewport 147 visible = { 148 "left": max(0, min(rect["x"], rect["x"] + rect["width"])), 149 "right": min(inner_width, max(rect["x"], rect["x"] + rect["width"])), 150 "top": max(0, min(rect["y"], rect["y"] + rect["height"])), 151 "bottom": min(inner_height, max(rect["y"], rect["y"] + rect["height"])), 152 } 153 154 # arrive at the centre point of the visible rectangle 155 x = (visible["left"] + visible["right"]) / 2.0 156 y = (visible["top"] + visible["bottom"]) / 2.0 157 158 # convert to CSS pixels, as centre point can be float 159 return (math.floor(x), math.floor(y)) 160 161 162 def document_hidden(session): 163 return session.execute_script("return document.hidden") 164 165 166 def document_location(session): 167 """ 168 Unlike ``webdriver.Session#url``, which always returns 169 the top-level browsing context's URL, this returns 170 the current browsing context's active document's URL. 171 """ 172 return session.execute_script("return document.location.href") 173 174 175 def element_rect(session, element): 176 return session.execute_script(""" 177 let element = arguments[0]; 178 let rect = element.getBoundingClientRect(); 179 180 return { 181 x: rect.left + window.pageXOffset, 182 y: rect.top + window.pageYOffset, 183 width: rect.width, 184 height: rect.height, 185 }; 186 """, args=(element,)) 187 188 189 def is_element_in_viewport(session, element): 190 """Check if element is outside of the viewport""" 191 return session.execute_script(""" 192 let el = arguments[0]; 193 194 let rect = el.getBoundingClientRect(); 195 let viewport = { 196 height: window.innerHeight || document.documentElement.clientHeight, 197 width: window.innerWidth || document.documentElement.clientWidth, 198 }; 199 200 return !(rect.right < 0 || rect.bottom < 0 || 201 rect.left > viewport.width || rect.top > viewport.height) 202 """, args=(element,)) 203 204 205 def is_fullscreen(session): 206 # At the time of writing, WebKit does not conform to the 207 # Fullscreen API specification. 208 # 209 # Remove the prefixed fallback when 210 # https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed. 211 return session.execute_script(""" 212 return !!(window.fullScreen || document.webkitIsFullScreen) 213 """) 214 215 216 def _get_maximized_state(session): 217 dimensions = session.execute_script(""" 218 return { 219 availWidth: screen.availWidth, 220 availHeight: screen.availHeight, 221 windowWidth: window.outerWidth, 222 windowHeight: window.outerHeight, 223 } 224 """) 225 226 # The maximized window can still have a border attached which would 227 # cause its dimensions to exceed the whole available screen. 228 return (dimensions["windowWidth"] >= dimensions["availWidth"] and 229 dimensions["windowHeight"] >= dimensions["availHeight"] and 230 # Only return true if the window is not in fullscreen mode 231 not is_fullscreen(session) 232 ) 233 234 235 def is_maximized(session, original_rect): 236 if _get_maximized_state(session): 237 return True 238 239 # Wayland doesn't guarantee that the window will get maximized 240 # to the screen, so check if the dimensions got larger. 241 elif is_wayland(): 242 dimensions = session.execute_script(""" 243 return { 244 windowWidth: window.outerWidth, 245 windowHeight: window.outerHeight, 246 } 247 """) 248 return ( 249 dimensions["windowWidth"] > original_rect["width"] and 250 dimensions["windowHeight"] > original_rect["height"] and 251 # Only return true if the window is not in fullscreen mode 252 not is_fullscreen(session) 253 ) 254 else: 255 return False 256 257 258 def is_not_maximized(session): 259 return not _get_maximized_state(session) 260 261 262 def is_wayland(): 263 # We don't use mozinfo.display here to make sure it also 264 # works upstream in wpt Github repo. 265 return os.environ.get("WAYLAND_DISPLAY", "") != "" 266 267 268 def filter_dict(source, d): 269 """Filter `source` dict to only contain same keys as `d` dict. 270 271 :param source: dictionary to filter. 272 :param d: dictionary whose keys determine the filtering. 273 """ 274 return {k: source[k] for k in d.keys()} 275 276 277 def filter_supported_key_events(all_events, expected): 278 events = [filter_dict(e, expected[0]) for e in all_events] 279 if len(events) > 0 and events[0]["code"] is None: 280 # Remove 'code' entry if browser doesn't support it 281 expected = [filter_dict(e, {"key": "", "type": ""}) for e in expected] 282 events = [filter_dict(e, expected[0]) for e in events] 283 284 return (events, expected) 285 286 287 def get_origin_from_url(url): 288 parsed_uri = urlparse(url) 289 return '{uri.scheme}://{uri.netloc}'.format(uri=parsed_uri) 290 291 292 def wait_for_new_handle(session, handles_before): 293 def find_new_handle(session): 294 new_handles = list(set(session.handles) - set(handles_before)) 295 assert len(new_handles) == 1, "No new window was opened" 296 return new_handles[0] 297 298 wait = Poll(session, timeout=5) 299 return wait.until(find_new_handle) 300 301 302 def get_extension_path(filename): 303 return os.path.join( 304 os.path.abspath(os.path.dirname(__file__)), "webextensions", filename 305 ) 306 307 308 def get_base64_for_extension_file(filename): 309 with open( 310 get_extension_path(filename), 311 "rb", 312 ) as file: 313 return base64.b64encode(file.read()).decode("utf-8")