def check_image(self, image, tag=None, ignore_mismatch=False): # type: (Union[Image.Image, Text], Optional[Text], bool) -> Optional[bool] if self.is_disabled: return None logger.info("check_image(Image {}, tag {}, ignore_mismatch {}".format( image, tag, ignore_mismatch)) return self._check_image(tag, ignore_mismatch, Target().image(image))
def _full_page_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info("Full page screenshot requested") screenshot = self._driver.get_full_page_screenshot( self._seconds_to_wait_screenshot, scale_provider) return EyesWebDriverScreenshot.create_from_image( screenshot, self._driver)
def check_region_by_element(self, element, tag=None, match_timeout=-1, target=None, stitch_content=False): # type: (AnyWebElement, tp.Optional[str], int, tp.Optional[Target], bool) -> None """ Takes a snapshot of the region of the given element from the browser using the web driver and matches it with the expected output. :param element: (WebElement) The element which region will be visually validated. :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.info("check_region_by_element('%s')" % tag) self._screenshot_type = self._obtain_screenshot_type( is_element=True, inside_a_frame=bool(self._driver.get_frame_chain()), stitch_content=stitch_content, force_fullpage=self.force_full_page_screenshot) self._region_to_check = element self._check_window_base(tag, match_timeout, target)
def check_region_by_element(self, element, tag=None, match_timeout=-1, target=None, stitch_content=False): # type: (AnyWebElement, tp.Optional[tp.Text], int, tp.Optional[Target], bool) -> None """ Takes a snapshot of the region of the given element from the browser using the web driver and matches it with the expected output. :param element: (WebElement) The element which region will be visually validated. :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.info("check_region_by_element('%s')" % tag) self._screenshot_type = self._obtain_screenshot_type(is_element=True, inside_a_frame=bool(self._driver.frame_chain), stitch_content=stitch_content, force_fullpage=self.force_full_page_screenshot) self._element_position_provider = ElementPositionProvider(self._driver, element) origin_overflow = element.get_overflow() eyes_selenium_utils.add_data_overflow_to_element(self.driver, element, origin_overflow) element.set_overflow('hidden') element_region = self._get_element_region(element) self._region_to_check = element_region self._check_window_base(tag, match_timeout, target) self._element_position_provider = None if origin_overflow: element.set_overflow(origin_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 check_region(self, region, tag=None, match_timeout=-1, target=None, stitch_content=False): # type: (Region, Optional[Text], int, Optional[Target], bool) -> None """ Takes a snapshot of the given region from the browser using the web driver and matches it with the expected output. If the current context is a frame, the region is offsetted relative to the frame. :param region: The region which will be visually validated. The coordinates are relative to the viewport of the current frame. :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.info("check_region([%s], '%s')" % (region, tag)) if region.is_empty: raise EyesError("region cannot be empty!") if target is None: target = Target() self._screenshot_type = self._obtain_screenshot_type( is_element=False, inside_a_frame=bool(self._driver.frame_chain), stitch_content=stitch_content, force_fullpage=self.force_full_page_screenshot, is_region=True, ) self._region_to_check = region self._check_window_base(tag, match_timeout, target)
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 _region_or_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info('Not entire element screenshot requested') screenshot = self._viewport_screenshot(scale_provider) region = screenshot.get_element_region_in_frame_viewport(self._region_to_check) screenshot = screenshot.get_sub_screenshot_by_region(region) return screenshot
def _entire_element_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info('Entire element screenshot requested') screenshot = self._driver.get_stitched_screenshot(self._region_to_check, self._seconds_to_wait_screenshot, scale_provider) return EyesWebDriverScreenshot.create_from_image(screenshot, self._driver)
def get_entire_size(self): try: size = {'width': self._element.get_scroll_width(), 'height': self._element.get_scroll_height()} except WebDriverException: raise EyesError('Failed to extract entire size!') logger.info("ElementPositionProvider - Entire size: {}".format(size)) return 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 user_agent(self): try: user_agent = self.driver.execute_script("return navigator.userAgent") logger.info("user agent: {}".format(user_agent)) except Exception as e: logger.info("Failed to obtain user-agent string") user_agent = None return user_agent
def check_region(self, image, region, tag=None, ignore_mismatch=False): # type: (Image.Image, Region, Optional[Text], bool) -> Optional[bool] if self.is_disabled: return None logger.info( "check_region(Image {}, region {}, tag {}, ignore_mismatch {}". format(image, region, tag, ignore_mismatch)) return self._check_image(tag, ignore_mismatch, Target().region(image, region))
def _download_css(base_url, value): # type: (str, str) -> str logger.info("Given URL to download: {}".format(value)) if (general_utils.is_absolute_url(value) and not general_utils.is_url_with_scheme(value)): url = urljoin('http://', value) else: url = urljoin(base_url, value) logger.info("Download CSS from {}".format(url)) return requests.get(url).text
def _region_or_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info('Not entire element screenshot requested') screenshot = self._viewport_screenshot(scale_provider) if isinstance(self._region_to_check, Region): screenshot = screenshot.get_sub_screenshot_by_region( self._region_to_check) else: screenshot = screenshot.get_sub_screenshot_by_element( self._region_to_check) return screenshot
def _viewport_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info('Viewport screenshot requested') screenshot64 = self._driver.get_screesnhot_as_base64_from_main_frame( self.seconds_to_wait_screenshot) screenshot = image_utils.image_from_bytes( base64.b64decode(screenshot64)) scale_provider.update_scale_ratio(screenshot.width) pixel_ratio = 1 / scale_provider.scale_ratio if pixel_ratio != 1.0: screenshot = image_utils.scale_image(screenshot, 1.0 / pixel_ratio) return EyesWebDriverScreenshot.create_from_image( screenshot, self._driver).get_viewport_screenshot()
def _get_environment(self): os = self.host_os # If no host OS was set, check for mobile OS. if os is None: logger.info('No OS set, checking for mobile OS...') # Since in Python Appium driver is the same for Android and iOS, we need to use the desired # capabilities to figure this out. if eyes_selenium_utils.is_mobile_device(self._driver): platform_name = self._driver.platform_name logger.info(platform_name + ' detected') platform_version = self._driver.platform_version if platform_version is not None: # Notice that Python's "split" function's +limit+ is the the maximum splits performed # whereas in Ruby it is the maximum number of elements in the result (which is why they are set # differently). major_version = platform_version.split('.', 1)[0] os = platform_name + ' ' + major_version else: os = platform_name logger.info("Setting OS: " + os) else: logger.info('No mobile OS detected.') app_env = { 'os': os, 'hostingApp': self.host_app, 'displaySize': self._viewport_size, 'inferred': self._get_inferred_environment() } return app_env
def _environment(self): os = self.host_os # If no host OS was set, check for mobile OS. if os is None: logger.info("No OS set, checking for mobile OS...") # Since in Python Appium driver is the same for Android and iOS, # we need to use the desired capabilities to figure this out. if eyes_selenium_utils.is_mobile_device(self._driver): platform_name = self._driver.platform_name logger.info(platform_name + " detected") platform_version = self._driver.platform_version if platform_version is not None: # Notice that Python's "split" function's +limit+ is the the # maximum splits performed whereas in Ruby it is the maximum # number of elements in the result (which is why they are set # differently). major_version = platform_version.split(".", 1)[0] os = platform_name + " " + major_version else: os = platform_name logger.info("Setting OS: " + os) else: logger.info("No mobile OS detected.") app_env = { "os": os, "hostingApp": self.host_app, "displaySize": self._viewport_size, "inferred": self._inferred_environment, } return app_env
def _viewport_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info('Viewport screenshot requested') self._driver._wait_before_screenshot(self._seconds_to_wait_screenshot) if not self._driver.is_mobile_device(): image64 = self._driver.get_screesnhot_as_base64_from_main_frame() else: image64 = self._driver.get_screenshot_as_base64() image = image_utils.image_from_bytes(base64.b64decode(image64)) scale_provider.update_scale_ratio(image.width) pixel_ratio = 1 / scale_provider.scale_ratio if pixel_ratio != 1.0: image = image_utils.scale_image(image, 1.0 / pixel_ratio) return EyesWebDriverScreenshot.create_from_image(image, self._driver).get_viewport_screenshot()
def get_viewport_size(driver): # type: (AnyWebDriver) -> ViewPort """ Tries to get the viewport size using Javascript. If fails, gets the entire browser window size! :param driver: The webdriver to use for getting the viewport size. """ # noinspection PyBroadException try: width, height = driver.execute_script(_JS_GET_VIEWPORT_SIZE) return dict(width=width, height=height) except WebDriverException: logger.info( "Failed to get viewport size. Only window size is available") return get_window_size(driver)
def check_window(self, tag=None, match_timeout=-1, target=None): # type: (tp.Optional[tp.Text], int, tp.Optional[Target]) -> None """ Takes a snapshot from the browser using the web driver and matches it with the expected output. :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.info("check_window('%s')" % tag) self._screenshot_type = self._obtain_screenshot_type(is_element=False, inside_a_frame=bool(self._driver.frame_chain), stitch_content=False, force_fullpage=self.force_full_page_screenshot) self._check_window_base(tag, match_timeout, target)
def _get_frame_bundled_css(driver): # type: (EyesWebDriver) -> tp.Text base_url = driver.current_url # type: ignore if not general_utils.is_absolute_url(base_url): logger.info("Base URL is not an absolute URL!") cssom_results = driver.execute_script(_CAPTURE_CSSOM_SCRIPT) raw_css_nodes = [ CssNode.create(base_url, css_href, css_text) for css_text, css_href in cssom_results ] if len(raw_css_nodes) > 5: pool = mp.Pool(processes=mp.cpu_count() * 2) results = pool.map(_process_raw_css_node, raw_css_nodes) else: results = [_process_raw_css_node(node) for node in raw_css_nodes] return "".join(results)
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 _serialize_css(base_url, stylesheet): # type: (str, tp.List) -> str css_string = '' for node in stylesheet: add_as_is = True if node.type == 'at-rule' and node.lower_at_keyword == 'import': logger.info("encountered @import rule") href = None for tag in node.prelude: if isinstance(tag, URLToken): href = tag.value css = _download_css(base_url, href) css = css.strip() logger.info('imported CSS (whitespaces trimmed) length: {}'.format(len(css))) add_as_is = len(css) == 0 if not add_as_is: css_string += css if add_as_is: css_string += node.serialize() return css_string
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 set_browser_size(driver, required_size): # type: (AnyWebDriver, ViewPort) -> bool retries_left = _RETRIES # set browser size for mobile devices isn't working if is_mobile_device(driver): return True while True: logger.info("Trying to set browser size to: " + str(required_size)) set_window_size(driver, required_size) time.sleep(_SLEEP) current_size = get_window_size(driver) logger.info("Current browser size: " + str(required_size)) if retries_left or current_size != required_size: break retries_left -= 1 return current_size == required_size
def get_viewport_size_or_display_size(driver): logger.info("get_viewport_size_or_display_size()") try: return get_viewport_size(driver) except Exception as e: logger.info( "Failed to extract viewport size using Javascript: {}".format( str(e))) # If we failed to extract the viewport size using JS, will use the # window size instead. logger.info("Using window size as viewport size.") window_size = get_window_size(driver) width = window_size["width"] height = window_size["height"] try: if is_landscape_orientation(driver) and height > width: height, width = width, height except WebDriverException: # Not every WebDriver supports querying for orientation. pass logger.info("Done! Size {:d} x {:d}".format(width, height)) return dict(width=width, height=height)
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