def check_region_by_selector(self, by, value, tag=None, match_timeout=-1, target=None, stitch_content=False): # type: (Text, Text, Optional[Text], int, Optional[Target], bool) -> None """ Takes a snapshot of the region of the element found by calling find_element (by, value) and matches it with the expected output. :param by: The way by which an element to be validated should be found (e.g., By.ID). :param value: The value identifying the element using the "by" type. :param tag: Description of the visual validation checkpoint. :param match_timeout: Timeout for the visual validation checkpoint (milliseconds). :param target: The target for the check_window call :return: None """ logger.debug("calling 'check_region_by_selector'...") # hack: prevent stale element exception by saving viewport value # before catching element self._driver.get_default_content_viewport_size() self.check_region_by_element( self._driver.find_element(by, value), tag, match_timeout, target, stitch_content, )
def _get_frame_dom(driver, args_obj): # type: (EyesWebDriver, tp.Dict) -> tp.Dict dom_tree = driver.execute_script(_CAPTURE_FRAME_SCRIPT, args_obj) base_url = driver.current_url # type: ignore logger.debug('Traverse DOM Tree') _traverse_dom_tree(driver, args_obj, dom_tree, -1, base_url) return dom_tree
def is_landscape_orientation(driver): if is_mobile_device(driver): # could be AppiumRemoteWebDriver appium_driver = get_underlying_driver(driver) # type: WebDriver original_context = None try: # We must be in native context in order to ask for orientation, # because of an Appium bug. original_context = appium_driver.context if (len(appium_driver.contexts) > 1 and not original_context.uppar() == "NATIVE_APP"): appium_driver.switch_to.context("NATIVE_APP") else: original_context = None except WebDriverException: original_context = None try: orieintation = appium_driver.orientation return orieintation.lower() == "landscape" except Exception: logger.debug( "WARNING: Couldn't get device orientation. Assuming Portrait.") finally: if original_context is not None: appium_driver.switch_to.context(original_context) return False
def set_overflow(self, overflow, stabilization_time=None): """ Sets the overflow of the current element. :param overflow: The overflow value to set. If the given value is None, then overflow will be set to undefined. :param stabilization_time: The time to wait for the page to stabilize after overflow is set. If the value is None, then no waiting will take place. (Milliseconds) :return: The previous overflow value. """ logger.debug("Setting overflow: %s" % overflow) if overflow is None: script = ( "var elem = arguments[0]; var origOverflow = elem.style.overflow; " "elem.style.overflow = undefined; " "return origOverflow;") else: script = ( "var elem = arguments[0]; var origOverflow = elem.style.overflow; " 'elem.style.overflow = "{0}"; ' "return origOverflow;".format(overflow)) # noinspection PyUnresolvedReferences original_overflow = self._driver.execute_script(script, self.element) logger.debug("Original overflow: %s" % original_overflow) if stabilization_time is not None: time.sleep(stabilization_time / 1000) return original_overflow
def check_region_in_frame_by_selector(self, frame_reference, # type: FrameReference by, # type: tp.Text value, # type: tp.Text tag=None, # type: tp.Optional[tp.Text] match_timeout=-1, # type: int target=None, # type: tp.Optional[Target] stitch_content=False # type: bool ): # type: (...) -> None """ Checks a region within a frame, and returns to the current frame. :param frame_reference: (int/str/WebElement) A reference to the frame in which the region should be checked. :param by: (By) The way by which an element to be validated should be found (e.g., By.ID). :param value: (str) The value identifying the element using the "by" type. :param tag: (str) Description of the visual validation checkpoint. :param match_timeout: (int) Timeout for the visual validation checkpoint (milliseconds). :param target: (Target) The target for the check_window call :return: None """ # TODO: remove this disable if self.is_disabled: logger.info('check_region_in_frame_by_selector(): ignored (disabled)') return logger.info("check_region_in_frame_by_selector('%s')" % tag) # Switching to the relevant frame with self._driver.switch_to.frame_and_back(frame_reference): logger.debug("calling 'check_region_by_selector'...") self.check_region_by_selector(by, value, tag, match_timeout, target, stitch_content)
def set_overflow(self, overflow, stabilization_time=None): # type: (tp.Text, tp.Optional[int]) -> tp.Text """ Sets the overflow of the current context's document element. :param overflow: The overflow value to set. If the given value is None, then overflow will be set to undefined. :param stabilization_time: The time to wait for the page to stabilize after overflow is set. If the value is None, then no waiting will take place. (Milliseconds) :return: The previous overflow value. """ logger.debug("Setting overflow: %s" % overflow) if overflow is None: script = "var origOverflow = document.documentElement.style.overflow; " \ "document.documentElement.style.overflow = undefined; " \ "return origOverflow;" else: script = "var origOverflow = document.documentElement.style.overflow; " \ "document.documentElement.style.overflow = \"{0}\"; " \ "return origOverflow;".format(overflow) # noinspection PyUnresolvedReferences original_overflow = self.driver.execute_script(script) logger.debug("Original overflow: %s" % original_overflow) if stabilization_time is not None: time.sleep(stabilization_time / 1000) eyes_selenium_utils.add_data_overflow_to_element( self.driver, None, original_overflow) return original_overflow
def _traverse_dom_tree(driver, args_obj, dom_tree, frame_index, base_url): # type: (EyesWebDriver, dict, dict, int, str) -> None logger.debug('Traverse DOM Tree: index_tree {}'.format(frame_index)) tag_name = dom_tree.get('tagName', None) # type: str if not tag_name: return None if frame_index > -1: driver.switch_to.frame(frame_index) dom = driver.execute_script(_CAPTURE_FRAME_SCRIPT, args_obj) dom_tree['childNodes'] = dom src_url = None attrs_node = dom_tree.get('attributes', None) if attrs_node: src_url = attrs_node.get('eyes_core', None) if src_url is None: logger.warning('IFRAME WITH NO SRC') _traverse_dom_tree(driver, args_obj, dom, -1, src_url) driver.switch_to.parent_frame() is_html = tag_name.upper() == 'HTML' if is_html: logger.debug('Traverse DOM Tree: Inside HTML') css = _get_frame_bundled_css(driver, base_url) dom_tree['css'] = css _loop(driver, args_obj, dom_tree, base_url)
def _update_scaling_params(self): # type: () -> Optional[ScaleProvider] if self._device_pixel_ratio != self._UNKNOWN_DEVICE_PIXEL_RATIO: logger.debug("Device pixel ratio was already changed") return None logger.info("Trying to extract device pixel ratio...") try: device_pixel_ratio = image_utils.get_device_pixel_ratio( self._driver) except Exception as e: logger.info( "Failed to extract device pixel ratio! Using default. Error %s " % e) device_pixel_ratio = self._DEFAULT_DEVICE_PIXEL_RATIO logger.info("Device pixel ratio: {}".format(device_pixel_ratio)) logger.info("Setting scale provider...") try: scale_provider = ContextBasedScaleProvider( top_level_context_entire_size=self._driver. get_entire_page_size(), viewport_size=self._get_viewport_size(), device_pixel_ratio=device_pixel_ratio, # always False as in Java version is_mobile_device=False, ) # type: ScaleProvider except Exception: # This can happen in Appium for example. logger.info("Failed to set ContextBasedScaleProvider.") logger.info("Using FixedScaleProvider instead...") scale_provider = FixedScaleProvider(1 / device_pixel_ratio) logger.info("Done!") return scale_provider
def open(self, driver, app_name, test_name, viewport_size=None): # type: (AnyWebDriver, Text, Text, Optional[ViewPort]) -> EyesWebDriver if self.is_disabled: logger.debug("open(): ignored (disabled)") return driver if isinstance(driver, EyesWebDriver): # If the driver is an EyesWebDriver (as might be the case when tests are ran # consecutively using the same driver object) self._driver = driver else: if not isinstance(driver, RemoteWebDriver): logger.info( "WARNING: driver is not a RemoteWebDriver (class: {0})". format(driver.__class__)) self._driver = EyesWebDriver(driver, self, self._stitch_mode) if viewport_size is not None: self._viewport_size = viewport_size eyes_selenium_utils.set_viewport_size(self._driver, viewport_size) self._ensure_viewport_size() self._open_base(app_name, test_name, viewport_size) return self._driver
def set_position(self, location): translate_command = "translate(-{}px, -{}px)".format(location.x, location.y) logger.debug(translate_command) transform_list = dict( (key, translate_command) for key in self._JS_TRANSFORM_KEYS ) self._set_transform(transform_list) self._current_position = location.clone()
def hide_scrollbars(self): # type: () -> tp.Text """ Hides the scrollbars of the current element. :return: The previous value of the overflow property (could be None). """ logger.debug("EyesWebElement.HideScrollbars()") return self.set_overflow("hidden")
def timed(*args, **kw): ts = time.time() result = method(*args, **kw) te = time.time() if 'log_time' in kw: name = kw.get('log_name', method.__name__.upper()) kw['log_time'][name] = int((te - ts) * 1000) else: logger.debug('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) return result
def hide_scrollbars(self): # type: () -> tp.Text """ Hides the scrollbars of the current context's document element. :return: The previous value of the overflow property (could be None). """ logger.debug('HideScrollbars() called. Waiting for page load...') self.wait_for_page_load() logger.debug('About to hide scrollbars') return self.set_overflow('hidden')
def set_browser_size_by_viewport_size(driver, actual_viewport_size, required_size): # type: (AnyWebDriver, ViewPort, ViewPort) -> bool browser_size = get_window_size(driver) logger.debug("Current browser size: {}".format(browser_size)) required_browser_size = dict( width=browser_size['width'] + (required_size['width'] - actual_viewport_size['width']), height=browser_size['height'] + (required_size['height'] - actual_viewport_size['height'])) return set_browser_size(driver, required_browser_size)
def add_text_trigger_by_element(self, element, text): # type: (AnyWebElement, Text) -> None """ Adds a text trigger. :param element: The element to which the text was sent. :param text: The trigger's text. """ if self.is_disabled: logger.debug("add_text_trigger: Ignoring '%s' (disabled)" % text) return # Triggers are activated on the last checked window. if self._last_screenshot is None: logger.debug("add_text_trigger: Ignoring '%s' (no screenshot)" % text) return if not self._driver.frame_chain == self._last_screenshot.frame_chain: logger.debug("add_text_trigger: Ignoring %s (different frame)" % text) return control = self._last_screenshot.get_intersected_region_by_element( element) # Making sure the trigger is within the last screenshot bounds if control.is_empty: logger.debug("add_text_trigger: Ignoring %s (out of bounds)" % text) return trigger = TextTrigger(control, text) self._user_inputs.append(trigger) logger.info("add_text_trigger: Added %s" % trigger)
def add_mouse_trigger_by_element(self, action, element): # type: (Text, AnyWebElement) -> None """ Adds a mouse trigger. :param action: Mouse action (click, double click etc.) :param element: The element on which the action was performed. """ if self.is_disabled: logger.debug("add_mouse_trigger: Ignoring %s (disabled)" % action) return # Triggers are activated on the last checked window. if self._last_screenshot is None: logger.debug("add_mouse_trigger: Ignoring %s (no screenshot)" % action) return if not self._driver.frame_chain == self._last_screenshot.frame_chain: logger.debug("add_mouse_trigger: Ignoring %s (different frame)" % action) return control = self._last_screenshot.get_intersected_region_by_element( element) # Making sure the trigger is within the last screenshot bounds if control.is_empty: logger.debug("add_mouse_trigger: Ignoring %s (out of bounds)" % action) return cursor = control.middle_offset trigger = MouseTrigger(action, control, cursor) self._user_inputs.append(trigger) logger.info("add_mouse_trigger: Added %s" % trigger)
def wait_for_page_load(self, timeout=3, throw_on_timeout=False): # type: (int, bool) -> None """ Waits for the current document to be "loaded". :param timeout: The maximum time to wait, in seconds. :param throw_on_timeout: Whether to throw an exception when timeout is reached. """ # noinspection PyBroadException try: WebDriverWait(self.driver, timeout) \ .until(lambda driver: driver.execute_script('return document.readyState') == 'complete') except Exception: logger.debug('Page load timeout reached!') if throw_on_timeout: raise
def get_full_window_dom(driver, return_as_dict=False): # type: (EyesWebDriver, bool) -> tp.Union[str, dict] dom_tree = json.loads( driver.execute_script(_CAPTURE_FRAME_SCRIPT, _ARGS_OBJ), object_pairs_hook=OrderedDict, ) logger.debug("Traverse DOM Tree") _traverse_dom_tree(driver, { "childNodes": [dom_tree], "tagName": "OUTER_HTML" }) if return_as_dict: return dom_tree return json.dumps(dom_tree)
def driver(request, browser_config): test_name = request.node.name build_tag = os.environ.get("BUILD_TAG", None) tunnel_id = os.environ.get("TUNNEL_IDENTIFIER", None) username = os.environ.get("SAUCE_USERNAME", None) access_key = os.environ.get("SAUCE_ACCESS_KEY", None) force_remote = request.config.getoption("remote", default=False) selenium_url = os.environ.get("SELENIUM_SERVER_URL", "http://127.0.0.1:4444/wd/hub") if "ondemand.saucelabs.com" in selenium_url or force_remote: selenium_url = "https://%s:%[email protected]:443/wd/hub" % ( username, access_key, ) logger.debug("SELENIUM_URL={}".format(selenium_url)) desired_caps = browser_config.copy() desired_caps["build"] = build_tag desired_caps["tunnelIdentifier"] = tunnel_id desired_caps["name"] = test_name executor = RemoteConnection(selenium_url, resolve_ip=False) browser = webdriver.Remote( command_executor=executor, desired_capabilities=desired_caps ) if browser is None: raise WebDriverException("Never created!") yield browser # report results try: browser.execute_script( "sauce:job-result=%s" % str(not request.node.rep_call.failed).lower() ) except WebDriverException: # we can ignore the exceptions of WebDriverException type -> We're done with tests. logger.info( "Warning: The driver failed to quit properly. Check test and server side logs." ) finally: browser.quit()
def get_screenshot_as_base64(self): # type: () -> tp.Text """ Gets the screenshot of the current window as a base64 encoded string which is useful in embedded images in HTML. """ screenshot64 = self.driver.get_screenshot_as_base64() display_rotation = self.get_display_rotation() if display_rotation != 0: logger.info('Rotation required.') # num_quadrants = int(-(display_rotation / 90)) logger.debug('Done! Creating image object...') screenshot = image_utils.image_from_base64(screenshot64) # rotating if display_rotation == -90: screenshot64 = image_utils.get_base64(screenshot.rotate(90)) logger.debug('Done! Rotating...') return screenshot64
def get_full_window_dom(driver, return_as_dict=False): # type: (EyesWebDriver, bool) -> tp.Union[str, dict] dom_tree = json.loads(driver.execute_script(_CAPTURE_FRAME_SCRIPT, _ARGS_OBJ), object_pairs_hook=OrderedDict) logger.debug('Traverse DOM Tree') _traverse_dom_tree(driver, { 'childNodes': [dom_tree], 'tagName': 'OUTER_HTML' }) # After traversing page could be scrolled down. Reset to origin position driver.reset_origin() if return_as_dict: return dom_tree return json.dumps(dom_tree)
def check_region_by_selector(self, by, value, tag=None, match_timeout=-1, target=None, stitch_content=False): # type: (str, str, tp.Optional[str], int, tp.Optional[Target], bool) -> None """ Takes a snapshot of the region of the element found by calling find_element(by, value) and matches it with the expected output. :param by: (By) The way by which an element to be validated should be found (e.g., By.ID). :param value: (str) The value identifying the element using the "by" type. :param tag: (str) Description of the visual validation checkpoint. :param match_timeout: (int) Timeout for the visual validation checkpoint (milliseconds). :param target: (Target) The target for the check_window call :return: None """ logger.debug("calling 'check_region_by_element'...") self.check_region_by_element(self._driver.find_element(by, value), tag, match_timeout, target, stitch_content)
def _parse_and_serialize_css(node, text, minimize=False): # type: (CssNode, tp.Text, bool) -> tp.Generator def is_import_node(n): return n.type == "at-rule" and n.lower_at_keyword == "import" stylesheet = tinycss2.parse_stylesheet(text, skip_comments=True, skip_whitespace=True) for style_node in stylesheet: if is_import_node(style_node): for tag in style_node.prelude: if tag.type == "url": logger.debug("The node has import") yield CssNode.create_sub_node(parent_node=node, href=tag.value) continue try: if minimize: try: # remove whitespaces inside blocks style_node.content = [ tok for tok in style_node.content if tok.type != "whitespace" ] except AttributeError as e: logger.warning( "Cannot serialize item: {}, cause error: {}".format( style_node, str(e))) serialized = style_node.serialize() if minimize: serialized = (serialized.replace("\n", "").replace( "/**/", " ").replace(" {", "{")) except TypeError as e: logger.warning(str(e)) continue yield CssNode.create_serialized_node(text=serialized)
def _parse_and_serialize_css(node, text, minimize=False): # type: (CssNode, tp.Text, bool) -> tp.Generator is_import_node = lambda n: n.type == 'at-rule' and n.lower_at_keyword == 'import' stylesheet = tinycss2.parse_stylesheet(text, skip_comments=True, skip_whitespace=True) for style_node in stylesheet: if is_import_node(style_node): for tag in style_node.prelude: if tag.type == 'url': logger.debug('The node has import') yield CssNode.create_sub_node(parent_node=node, href=tag.value) continue try: if minimize: try: # remove whitespaces inside blocks style_node.content = [ tok for tok in style_node.content if tok.type != 'whitespace' ] except AttributeError as e: logger.warning( "Cannot serialize item: {}, cause error: {}".format( style_node, str(e))) serialized = style_node.serialize() if minimize: serialized = serialized.replace('\n', '').replace( '/**/', ' ').replace(' {', '{') except TypeError as e: logger.warning(str(e)) continue yield CssNode.create_serialized_node(text=serialized)
def _assign_viewport_size(self): # type: () -> None """ Assign the viewport size we need to be in the default content frame. """ original_frame_chain = self._driver.get_frame_chain() self._driver.switch_to.default_content() try: if self._viewport_size: logger.debug("Assigning viewport size {0}".format( self._viewport_size)) self.set_viewport_size(self._driver, self._viewport_size) else: logger.debug( "No viewport size given. Extracting the viewport size from the driver..." ) self._viewport_size = self.get_viewport_size() logger.debug("Viewport size {0}".format(self._viewport_size)) except EyesError: raise TestFailedError('Failed to assign viewport size!') finally: # Going back to the frame we started at self._driver.switch_to.frames(original_frame_chain)
def get_stitched_screenshot(self, element_region, wait_before_screenshots, scale_provider): # type: (Region, int, ScaleProvider) -> Image.Image """ Gets a stitched screenshot for specific element :param element_region: Region of required screenshot :param wait_before_screenshots: Seconds to wait before taking each screenshot. :param scale_provider: Scale image if needed. :return: The full element screenshot. """ logger.info('getting stitched element screenshot..') self._position_provider = self._eyes._element_position_provider entire_size = self._position_provider.get_entire_size() logger.debug("Element region: {}".format(element_region)) # Firefox 60 and above make a screenshot of the current frame when other browsers # make a screenshot of the viewport. So we scroll down to frame at _will_switch_to method # and add a left margin here. # TODO: Refactor code. Use EyesScreenshot if self._frame_chain: if ((self.browser_name == 'firefox' and self.browser_version < 60.0) or self.browser_name in ('chrome', 'MicrosoftEdge', 'internet explorer', 'safari')): element_region.left += int(self._frame_chain.peek.location.x) screenshot_part_size = { 'width': element_region.width, 'height': max(element_region.height - self._MAX_SCROLL_BAR_SIZE, self._MIN_SCREENSHOT_PART_HEIGHT) } entire_element = Region(0, 0, entire_size['width'], entire_size['height']) screenshot_parts = entire_element.get_sub_regions(screenshot_part_size) viewport = self.get_viewport_size() screenshot = image_utils.image_from_bytes( base64.b64decode(self.get_screenshot_as_base64())) scale_provider.update_scale_ratio(screenshot.width) pixel_ratio = 1 / scale_provider.scale_ratio need_to_scale = True if pixel_ratio != 1.0 else False if need_to_scale: element_region = element_region.scale( scale_provider.device_pixel_ratio) # Starting with element region size part of the screenshot. Use it as a size template. stitched_image = Image.new( 'RGBA', (entire_element.width, entire_element.height)) for part in screenshot_parts: logger.debug("Taking screenshot for {0}".format(part)) # Scroll to the part's top/left and give it time to stabilize. self._position_provider.set_position(Point(part.left, part.top)) EyesWebDriver._wait_before_screenshot(wait_before_screenshots) # Since screen size might cause the scroll to reach only part of the way current_scroll_position = self._position_provider.get_current_position( ) logger.debug("Scrolled To ({0},{1})".format( current_scroll_position.x, current_scroll_position.y)) part64 = self.get_screenshot_as_base64() part_image = image_utils.image_from_bytes(base64.b64decode(part64)) # Cut to viewport size the full page screenshot of main frame for some browsers if self._frame_chain: if (self.browser_name == 'firefox' and self.browser_version < 60.0 or self.browser_name in ('internet explorer', 'safari')): # TODO: Refactor this to make main screenshot only once frame_scroll_position = int( self._frame_chain.peek.location.y) part_image = image_utils.get_image_part( part_image, Region(top=frame_scroll_position, height=viewport['height'], width=viewport['width'])) # We cut original image before scaling to prevent appearing of artifacts part_image = image_utils.get_image_part(part_image, element_region) if need_to_scale: part_image = image_utils.scale_image(part_image, 1.0 / pixel_ratio) # first iteration if stitched_image is None: stitched_image = part_image continue stitched_image.paste(part_image, box=(current_scroll_position.x, current_scroll_position.y)) self._position_provider = self._origin_position_provider return stitched_image
def get_full_page_screenshot(self, wait_before_screenshots, scale_provider): # type: (Num, ScaleProvider) -> Image.Image """ Gets a full page screenshot. :param wait_before_screenshots: Seconds to wait before taking each screenshot. :return: The full page screenshot. """ logger.info('getting full page screenshot..') # Saving the current frame reference and moving to the outermost frame. original_frame = self.frame_chain self.switch_to.default_content() self.reset_origin() entire_page_size = self.get_entire_page_size() # Starting with the screenshot at 0,0 EyesWebDriver._wait_before_screenshot(wait_before_screenshots) part64 = self.get_screenshot_as_base64() screenshot = image_utils.image_from_bytes(base64.b64decode(part64)) scale_provider.update_scale_ratio(screenshot.width) pixel_ratio = 1.0 / scale_provider.scale_ratio need_to_scale = True if pixel_ratio != 1.0 else False if need_to_scale: screenshot = image_utils.scale_image(screenshot, 1.0 / pixel_ratio) # IMPORTANT This is required! Since when calculating the screenshot parts for full size, # we use a screenshot size which is a bit smaller (see comment below). if (screenshot.width >= entire_page_size['width']) and \ (screenshot.height >= entire_page_size['height']): self.restore_origin() self.switch_to.frames(original_frame) logger.debug("Entire page has size as screenshot") return screenshot # We use a smaller size than the actual screenshot size in order to eliminate duplication # of bottom scroll bars, as well as footer-like elements with fixed position. screenshot_part_size = { 'width': screenshot.width, 'height': max(screenshot.height - self._MAX_SCROLL_BAR_SIZE, self._MIN_SCREENSHOT_PART_HEIGHT) } logger.debug("Total size: {0}, Screenshot part size: {1}".format( entire_page_size, screenshot_part_size)) entire_page = Region(0, 0, entire_page_size['width'], entire_page_size['height']) screenshot_parts = entire_page.get_sub_regions(screenshot_part_size) # Starting with the screenshot we already captured at (0,0). stitched_image = Image.new('RGBA', (entire_page.width, entire_page.height)) stitched_image.paste(screenshot, box=(0, 0)) self.save_position() for part in screenshot_parts: # Since we already took the screenshot for 0,0 if part.left == 0 and part.top == 0: logger.debug('Skipping screenshot for 0,0 (already taken)') continue logger.debug("Taking screenshot for {0}".format(part)) # Scroll to the part's top/left and give it time to stabilize. self._position_provider.set_position(Point(part.left, part.top)) # self.scroll_to(Point(part.left, part.top)) EyesWebDriver._wait_before_screenshot(wait_before_screenshots) # Since screen size might cause the scroll to reach only part of the way current_scroll_position = self._position_provider.get_current_position( ) logger.debug("Scrolled To ({0},{1})".format( current_scroll_position.x, current_scroll_position.y)) part64 = self.get_screenshot_as_base64() part_image = image_utils.image_from_bytes(base64.b64decode(part64)) if need_to_scale: part_image = image_utils.scale_image(part_image, 1.0 / pixel_ratio) stitched_image.paste(part_image, box=(current_scroll_position.x, current_scroll_position.y)) self.restore_position() self.restore_origin() self.switch_to.frames(original_frame) return stitched_image
def get_stitched_screenshot(self, element, wait_before_screenshots, scale_provider): # type: (AnyWebElement, int, ScaleProvider) -> Image.Image """ Gets a stitched screenshot for specific element :param wait_before_screenshots: Seconds to wait before taking each screenshot. :return: The full page screenshot. """ logger.info('getting stitched element screenshot..') self._position_provider = ElementPositionProvider(self.driver, element) entire_size = self._position_provider.get_entire_size() # We use a smaller size than the actual screenshot size in order to eliminate duplication # of bottom scroll bars, as well as footer-like elements with fixed position. pl = element.location # TODO: add correct values for Safari # in the safari browser the returned size has absolute value but not relative as # in other browsers origin_overflow = element.get_overflow() element.set_overflow('hidden') element_width = element.get_client_width() element_height = element.get_client_height() border_left_width = element.get_computed_style_int('border-left-width') border_top_width = element.get_computed_style_int('border-top-width') element_region = Region(pl['x'] + border_left_width, pl['y'] + border_top_width, element_width, element_height) logger.debug("Element region: {}".format(element_region)) # Firefox 60 and above make a screenshot of the current frame when other browsers # make a screenshot of the viewport. So we scroll down to frame at _will_switch_to method # and add a left margin here. # TODO: Refactor code. Use EyesScreenshot if self._frames: if ((self.browser_name == 'firefox' and self.browser_version < 60.0) or self.browser_name in ('chrome', 'MicrosoftEdge', 'internet explorer', 'safari')): element_region.left += int(self._frames[-1].location['x']) screenshot_part_size = {'width': element_region.width, 'height': max(element_region.height - self._MAX_SCROLL_BAR_SIZE, self._MIN_SCREENSHOT_PART_HEIGHT)} entire_element = Region(0, 0, entire_size['width'], entire_size['height']) screenshot_parts = entire_element.get_sub_regions(screenshot_part_size) viewport = self.get_viewport_size() screenshot = image_utils.image_from_bytes(base64.b64decode(self.get_screenshot_as_base64())) scale_provider.update_scale_ratio(screenshot.width) pixel_ratio = 1 / scale_provider.scale_ratio need_to_scale = True if pixel_ratio != 1.0 else False if need_to_scale: element_region = element_region.scale(scale_provider.device_pixel_ratio) # Starting with element region size part of the screenshot. Use it as a size template. stitched_image = Image.new('RGBA', (entire_element.width, entire_element.height)) for part in screenshot_parts: logger.debug("Taking screenshot for {0}".format(part)) # Scroll to the part's top/left and give it time to stabilize. self._position_provider.set_position(Point(part.left, part.top)) EyesWebDriver._wait_before_screenshot(wait_before_screenshots) # Since screen size might cause the scroll to reach only part of the way current_scroll_position = self._position_provider.get_current_position() logger.debug("Scrolled To ({0},{1})".format(current_scroll_position.x, current_scroll_position.y)) part64 = self.get_screenshot_as_base64() part_image = image_utils.image_from_bytes(base64.b64decode(part64)) # Cut to viewport size the full page screenshot of main frame for some browsers if self._frames: if (self.browser_name == 'firefox' and self.browser_version < 60.0 or self.browser_name in ('internet explorer', 'safari')): # TODO: Refactor this to make main screenshot only once frame_scroll_position = int(self._frames[-1].location['y']) part_image = image_utils.get_image_part(part_image, Region(top=frame_scroll_position, height=viewport['height'], width=viewport['width'])) # We cut original image before scaling to prevent appearing of artifacts part_image = image_utils.get_image_part(part_image, element_region) if need_to_scale: part_image = image_utils.scale_image(part_image, 1.0 / pixel_ratio) # first iteration if stitched_image is None: stitched_image = part_image continue stitched_image.paste(part_image, box=(current_scroll_position.x, current_scroll_position.y)) if origin_overflow: element.set_overflow(origin_overflow) return stitched_image
def _wait_before_screenshot(seconds): logger.debug("Waiting {} ms before taking screenshot..".format( int(seconds * 1000))) time.sleep(seconds) logger.debug("Finished waiting!")
def set_position(self, location): scroll_command = "window.scrollTo({0}, {1})".format(location.x, location.y) logger.debug(scroll_command) self._execute_script(scroll_command)