def sub_screenshot(self, region, throw_if_clipped=False): # type: (Region, bool) -> EyesImagesScreenshot argument_guard.not_none(region) # We want to get the sub-screenshot in as-is coordinates type. sub_screenshot_region = self.intersected_region( region, self.SCREENSHOT_AS_IS) if sub_screenshot_region.is_size_empty and ( throw_if_clipped or not sub_screenshot_region.size == region.size): raise OutOfBoundsError( "Region [{}] is out of screenshot bounds [{}]".format( region, self._bounds)) sub_screenshot_image = image_utils.get_image_part( self._image, sub_screenshot_region) # Notice that we need the bounds-relative coordinates as parameter # for new sub-screenshot. relative_sub_screenshot_region = self.convert_region_location( sub_screenshot_region, self.SCREENSHOT_AS_IS, self.CONTEXT_RELATIVE) return EyesImagesScreenshot(sub_screenshot_image, relative_sub_screenshot_region.location)
def add_device_emulation(self, device_name, orientation=ScreenOrientation.PORTRAIT): # type: (DeviceName, ScreenOrientation) -> Configuration argument_guard.not_none(device_name) self.add_browser(ChromeEmulationInfo(device_name, orientation)) return self
def render_status_by_id(self, *render_ids): # type: (*Text) -> List[RenderStatusResults] argument_guard.not_none(render_ids) if self._render_info is None: raise EyesError("render_info must be fetched first") headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/json" headers["X-Auth-Token"] = self._render_info.access_token url = urljoin(self._render_info.service_url, self.RENDER_STATUS) response = self._com.request( requests.post, url, use_api_key=False, headers=headers, data=json.dumps(render_ids), ) if not response.ok: raise EyesError( "Error getting server status, {} {}".format( response.status_code, response.content ) ) # TODO: improve parser to handle similar names return json_utils.attr_from_response(response, RenderStatusResults)
def normalize_rotation(driver, image, rotation): # type: (WebDriver, Image, Optional[int]) -> Image """ Rotates the image as necessary. The rotation is either manually forced by passing a non-null rotation, or automatically inferred. :param driver: The underlying driver which produced the screenshot. :param image: The image to normalize. :param rotation: The degrees by which to rotate the image: positive values = clockwise rotation, negative values = counter-clockwise, 0 = force no rotation, null = rotate automatically as needed. :return: A normalized image. """ argument_guard.not_none(driver) argument_guard.not_none(image) normalized_image = image if rotation and rotation != 0: normalized_image = image_utils.rotate_image(image, rotation) else: # Do automatic rotation if necessary try: logger.info("Trying to automatically normalize rotation...") if (eyes_selenium_utils.is_mobile_app(driver) and eyes_selenium_utils.is_landscape_orientation(driver) and image.height > image.width): # For Android, we need to rotate images to the right, # and for iOS to the left. degree = 90 if eyes_selenium_utils.is_android( driver) else -90 normalized_image = image_utils.rotate_image(image, degree) except Exception as e: logger.exception(e) logger.info("Skipped automatic rotation handling.") return normalized_image
def get_locators(self, visual_locator_settings): # type: (VisualLocatorSettings) -> LOCATORS_TYPE argument_guard.not_none(visual_locator_settings) logger.info( "Get locators with given names: {}".format(visual_locator_settings.names) ) logger.info("Requested viewport screenshot for visual locators..") viewport_screenshot = self._get_viewport_screenshot() self._debug_screenshot_provider.save( viewport_screenshot, "Visual locators: {}".format(visual_locator_settings.names), ) image = image_utils.get_bytes(viewport_screenshot) logger.info("Post visual locators screenshot...") viewport_screenshot_url = self._server_connector.try_upload_image(image) logger.info("Screenshot URL: {}".format(viewport_screenshot_url)) data = VisualLocatorsData( app_name=self._eyes.configure.app_name, image_url=viewport_screenshot_url, first_only=visual_locator_settings.values.is_first_only, locator_names=visual_locator_settings.values.names, ) logger.info("Post visual locators: {}".format(data)) return self._server_connector.post_locators(data)
def get_text_regions(self, config): # type: (TextRegionSettings) -> PATTERN_TEXT_REGIONS def get_app_output(): scale_provider = self._eyes.update_scaling_params() viewport_screenshot = self._eyes.get_scaled_cropped_viewport_image( scale_provider) image = image_utils.get_bytes(viewport_screenshot) screenshot_url = self._server_connector.try_upload_image(image) dom_json = self._eyes._try_capture_dom() dom_url = self._eyes._try_post_dom_capture(dom_json) return AppOutput(dom_url=dom_url, screenshot_url=screenshot_url, location=Point.ZERO()) argument_guard.not_none(config._patterns) data = TextSettingsData( app_output=get_app_output(), patterns=config._patterns, ignore_case=config._ignore_case, first_only=config._first_only, language=config._language, ) return self._server_connector.get_text_regions_in_running_session_image( data)
def render_put_resource(self, resource): # type: (VGResource) -> Text argument_guard.not_none(resource) if self._render_info is None: raise EyesError("render_info must be fetched first") logger.debug("resource hash: {} url: {}".format(resource.hash, resource.url)) content = resource.content argument_guard.not_none(content) headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = resource.content_type headers["X-Auth-Token"] = self._render_info.access_token url = urljoin( self._render_info.service_url, self.RESOURCES_SHA_256 + resource.hash ) response = self._com.request( "put", url, use_api_key=False, headers=headers, data=content, params={"render-id": "NONE"}, ) logger.debug("ServerConnector.put_resource - request succeeded") if not response.ok: raise EyesError( "Error putting resource: {}, {}".format( response.status_code, response.content ) ) return resource.hash
def _do_switch_to_frame(self, target_frame): # type: (EyesWebElement) -> None """ Will be called when switching into a frame. :param target_frame: The element to be switched to. """ argument_guard.not_none(target_frame) size_and_borders = target_frame.size_and_borders borders = size_and_borders.borders frame_inner_size = size_and_borders.size bounds = target_frame.bounding_client_rect content_location = Point( bounds["x"] + borders["left"], bounds["y"] + borders["top"] ) outer_size = RectangleSize.from_(target_frame.size) inner_size = RectangleSize.from_(frame_inner_size) original_location = target_frame.scroll_location self._switch_to.frame(target_frame.element) scroll_root_element = self._driver.find_element_by_xpath("/*") frame = Frame( reference=target_frame, location=content_location, outer_size=outer_size, inner_size=inner_size, parent_scroll_position=original_location, scroll_root_element=scroll_root_element, ) self._driver.frame_chain.push(frame)
def open(self, driver): # type: (EyesWebDriver) -> EyesWebDriver self._test_uuid = uuid.uuid4() if self.configuration.is_disabled: return driver logger.open_() argument_guard.not_none(driver) logger.debug("VisualGridEyes.open(%s)" % self.configuration) self._driver = driver browsers_info = self.configuration.browsers_info if self.configuration.viewport_size: self._set_viewport_size(self.configuration.viewport_size) elif browsers_info: viewports = [bi.viewport_size for bi in browsers_info] if viewports: self.configuration.viewport_size = viewports[0] else: self.configuration.viewport_size = self._get_viewport_size() for b_info in browsers_info: test = RunningTest(self._create_vgeyes_connector(b_info), self.configuration, b_info) test.on_results_received( lambda r: self.vg_manager.aggregate_result(test, r)) test.test_uuid = self._test_uuid self.test_list.append(test) self._is_opened = True self.vg_manager.open(self) logger.info("VisualGridEyes opening {} tests...".format( len(self.test_list))) return driver
def intersected_region(self, region, coordinates_type): # type: (Region, CoordinatesType) -> Region argument_guard.not_none(region) argument_guard.not_none(coordinates_type) if region.is_size_empty: return Region.from_(region) original_coordinates_type = region.coordinates_type intersected_region = self.convert_region_location( region, original_coordinates_type, self.SCREENSHOT_AS_IS ) # If the request was context based, we intersect with the frame window. if original_coordinates_type in [self.CONTEXT_AS_IS, self.CONTEXT_RELATIVE]: intersected_region = intersected_region.intersect(self.frame_window) # If the request is screenshot based, we intersect with the image elif original_coordinates_type == self.SCREENSHOT_AS_IS: intersected_region = intersected_region.intersect( Region(0, 0, self.image.width, self.image.height) ) else: raise ValueError( "Unknown coordinates type: '%s'" % original_coordinates_type ) # If the intersection is empty we don't want to convert the coordinates. if intersected_region.is_size_empty: return intersected_region # Converting the result to the required coordinates type intersected_region = self.convert_region_location( intersected_region, self.SCREENSHOT_AS_IS, coordinates_type ) return intersected_region
def open(self, driver): # type: (EyesWebDriver) -> EyesWebDriver self._test_uuid = uuid.uuid4() argument_guard.not_none(driver) logger.debug("VisualGridEyes.open(%s)" % self.configure) self._driver = driver self._set_viewport_size() self.server_connector.update_config( self.configure, self.full_agent_id, ua_string=self.driver.user_agent.origin_ua_string, ) self.server_connector.render_info() self.vg_manager.rendering_service.maybe_set_server_connector( self.server_connector) for browser_info, job_info in self._job_info_for_browser_info( self.configure.browsers_info): test = RunningTest( self._create_vgeyes_connector(browser_info, job_info), self.configure.clone(), browser_info, self.vg_manager.rendering_service, ) test.on_results_received(self.vg_manager.aggregate_result) test.test_uuid = self._test_uuid self.test_list.append(test) test.becomes_not_opened() self._is_opened = True self.vg_manager.open(self) logger.info("VisualGridEyes opening {} tests...".format( len(self.test_list))) return driver
def add_device_emulation(self, device_name, orientation=ScreenOrientation.PORTRAIT): argument_guard.not_none(device_name) emu = ChromeEmulationInfo(device_name, orientation) self.add_browser(RenderBrowserInfo(emulation_info=emu)) return self
def __init__(self, runner, config): # type: (VisualGridRunner, Eyes)-> None self._config_provider = config self._elements = [] argument_guard.not_none(runner) self.vg_manager = runner self.test_list = [] # type: List[RunningTest]
def will_switch_to_frame(self, target_frame): # type: (EyesWebElement) -> None """ Will be called before switching into a frame. :param target_frame: The element about to be switched to. """ argument_guard.not_none(target_frame) pl = target_frame.location size_and_borders = target_frame.size_and_borders borders = size_and_borders.borders frame_inner_size = size_and_borders.size content_location = Point(pl["x"] + borders["left"], pl["y"] + borders["top"]) sp = ScrollPositionProvider( self._driver, self._driver.find_element_by_tag_name("html")) original_location = sp.get_current_position() sp.set_position(original_location) frame = Frame( target_frame, content_location, target_frame.size, frame_inner_size, parent_scroll_position=original_location, ) self._driver.frame_chain.push(frame)
def __init__(self, config_provider, runner): # type: (Eyes, VisualGridRunner)-> None self._config_provider = config_provider self._elements = [] argument_guard.not_none(runner) self.vg_manager = runner # type: VisualGridRunner self.test_list = [] # type: List[RunningTest] self._test_uuid = None self.server_connector = None # type: Optional[ServerConnector]
def __init__(self, config_provider, runner): # type: (Eyes, VisualGridRunner)-> None self._config_provider = config_provider self._elements = [] argument_guard.not_none(runner) self.vg_manager = runner # type: VisualGridRunner self.test_list = [] # type: List[RunningTest] self._test_uuid = None self.server_connector = ServerConnector() # type: ServerConnector self.resource_collection_queue = [ ] # type: List[ResourceCollectionTask]
def open( self, driver, # type: AnyWebDriver app_name=None, # type: Optional[Text] test_name=None, # type: Optional[Text] viewport_size=None, # type: Optional[ViewPort] ): # type: (...) -> Optional[EyesWebDriver] """ Starts a test. :param driver: The driver that controls the browser hosting the application under the test. :param app_name: The name of the application under test. :param test_name: The test name. :param viewport_size: The client's viewport size (i.e., the visible part of the document's body) or None to allow any viewport size. :raise EyesError: If the session was already open. """ if self.configure.is_disabled: logger.info("open(): ignored (disabled)") return if app_name: self.configure.app_name = app_name if test_name: self.configure.test_name = test_name if viewport_size: self.configure.viewport_size = viewport_size # type: ignore argument_guard.not_none( self.configure.app_name, ValueError("app_name is required") ) argument_guard.not_none( self.configure.test_name, ValueError("test_name is required") ) logger.info( "open(app_name={}, test_name={}, viewport_size={})".format( self.configure.app_name, self.configure.test_name, self.configure.viewport_size, ) ) self._init_driver(driver) self._init_additional_providers() if self._is_visual_grid_eyes: self._selenium_eyes.open(self._driver, skip_start_session=True) result = self._current_eyes.open(self.driver) self._is_opened = True return result
def delete_session(self, test_results): # type: (TestResults) -> None argument_guard.not_none(test_results) if None in (test_results.id, test_results.batch_id, test_results.secret_token): logger.error("Can't delete session, results are None") return self._com.request( "delete", "{}/{}/{}".format( self.API_SESSIONS_BATCHES, test_results.batch_id, test_results.id ), params={"AccessToken": test_results.secret_token}, ).raise_for_status()
def location_in_screenshot(self, location, coordinates_type): # type: (Point, CoordinatesType) -> Point argument_guard.not_none(location) argument_guard.not_none(coordinates_type) location = self.convert_location(location, coordinates_type, self.CONTEXT_RELATIVE) if not self._bounds.contains(location): raise OutOfBoundsError( "Point {} ('{}') is not visible in screenshot!".format( location, coordinates_type)) return self.convert_location(location, self.CONTEXT_RELATIVE, self.SCREENSHOT_AS_IS)
def open_base( self, app_name, # type: Text test_name, # type: Text viewport_size=None, # type: Optional[ViewPort] session_type=SessionType.SEQUENTIAL, # type: SessionType ): # type: (...) -> None """ Starts a test. :param app_name: The name of the application under test. :param test_name: The test name. :param viewport_size: The client's viewport size (i.e., the visible part of the document's body) or None to allow any viewport size. :param session_type: The type of test (e.g., Progression for timing tests) or Sequential by default. :return: An updated web driver :raise EyesError: If the session was already open. """ logger.open_() if self.configuration.is_disabled: logger.debug("open_base(): ignored (disabled)") return if self._server_connector is None: raise EyesError("Server connector not set.") # If there's no default application name, one must be provided for the current test. if self.configuration.app_name is None: argument_guard.not_none(app_name) self.configuration.app_name = app_name argument_guard.not_none(test_name) self.configuration.test_name = test_name logger.info("\nAgent: {}\n".format(self.full_agent_id)) logger.info( "open_base(%s, %s, %s, %s)" % (app_name, test_name, viewport_size, self.configuration.failure_reports) ) self.configuration.session_type = session_type self.configuration.viewport_size = viewport_size self._open_base()
def _try_upload_data(self, bytes_data, content_type, media_type): # type: (bytes, Text, Text) -> Optional[Text] argument_guard.not_none(bytes_data) rendering_info = self.render_info() if rendering_info and rendering_info.results_url: try: target_url = rendering_info.results_url guid = uuid.uuid4() target_url = target_url.replace("__random__", str(guid)) logger.debug("Uploading {} to {}".format(media_type, target_url)) if self._upload_data( bytes_data, rendering_info, target_url, content_type, media_type ): return target_url except Exception as e: logger.error("Error uploading {}".format(media_type)) logger.exception(e)
def intersected_region(self, region, coordinates_type): # type: (Region, CoordinatesType) -> Region argument_guard.not_none(region) argument_guard.not_none(coordinates_type) if region.is_size_empty: return Region.from_(region) intersected_region = self.convert_region_location( region, region.coordinates_type, self.CONTEXT_RELATIVE) intersected_region.intersect(self._bounds) if region.is_size_empty: return region intersected_region.location = self.convert_location( intersected_region.location, self.CONTEXT_RELATIVE, coordinates_type) return intersected_region
def open(self, driver): # type: (EyesWebDriver) -> EyesWebDriver self._test_uuid = uuid.uuid4() logger.open_() argument_guard.not_none(driver) logger.debug("VisualGridEyes.open(%s)" % self.configure) self._driver = driver self._set_viewport_size() for b_info in self.configure.browsers_info: test = RunningTest(self._create_vgeyes_connector(b_info), self.configure, b_info) test.on_results_received(self.vg_manager.aggregate_result) test.test_uuid = self._test_uuid self.test_list.append(test) self._is_opened = True self.vg_manager.open(self) logger.info("VisualGridEyes opening {} tests...".format( len(self.test_list))) return driver
def convert_location(self, location, from_, to): # type: (Point, CoordinatesType, CoordinatesType) -> Point argument_guard.not_none(location) argument_guard.not_none(from_) argument_guard.not_none(to) argument_guard.is_a(location, Point) result = location.clone() if from_ == to: return result if from_ == self.SCREENSHOT_AS_IS: if to == self.CONTEXT_RELATIVE: result = result.offset(self._bounds.left, self._bounds.top) else: raise CoordinatesTypeConversionError(from_, to) elif from_ == self.SCREENSHOT_AS_IS: if to == self.CONTEXT_RELATIVE: result = result.offset(-self._bounds.left, -self._bounds.top) else: raise CoordinatesTypeConversionError(from_, to) else: raise CoordinatesTypeConversionError(from_, to) return result
def convert_location(self, location, from_, to): # noqa: C901 # type: (Point, CoordinatesType, CoordinatesType) -> Point argument_guard.not_none(location) argument_guard.not_none(from_) argument_guard.not_none(to) result = Point.from_(location) if from_ == to: return result # If we're not inside a frame, and the screenshot is the entire # page, then the context as-is/relative are the same (notice # screenshot as-is might be different, e.g., # if it is actually a sub-screenshot of a region). if (len(self.frame_chain) == 0 and self._screenshot_type == ScreenshotType.ENTIRE_FRAME): if (from_ == self.CONTEXT_RELATIVE or from_ == self.CONTEXT_AS_IS) and to == self.SCREENSHOT_AS_IS: # If this is not a sub-screenshot, this will have no effect. result = result.offset(self._frame_location_in_screenshot) # If this is not a region subscreenshot, this will have no effect. result = result.offset(-self.region_window.left, -self.region_window.top) elif from_ == self.SCREENSHOT_AS_IS and ( to == self.CONTEXT_RELATIVE or to == self.CONTEXT_AS_IS): result = result.offset(-self._frame_location_in_screenshot) return result if from_ == self.CONTEXT_AS_IS: if to == self.CONTEXT_RELATIVE: result = result.offset(self._current_frame_scroll_position) elif to == self.SCREENSHOT_AS_IS: result = result.offset(self._frame_location_in_screenshot) else: raise CoordinatesTypeConversionError(from_, to) elif from_ == self.CONTEXT_RELATIVE: if to == self.SCREENSHOT_AS_IS: # First, convert context-relative to context-as-is. result = result.offset(-self._current_frame_scroll_position) # Now convert context-as-is to screenshot-as-is result = result.offset(self._frame_location_in_screenshot) elif to == self.CONTEXT_AS_IS: result = result.offset(-self._current_frame_scroll_position) else: raise CoordinatesTypeConversionError(from_, to) elif from_ == self.SCREENSHOT_AS_IS: if to == self.CONTEXT_RELATIVE: # First, convert context-relative to context-as-is. result = result.offset(-self._frame_location_in_screenshot) # Now convert context-as-is to screenshot-as-is result = result.offset(self._current_frame_scroll_position) elif to == self.CONTEXT_AS_IS: result = result.offset(-self._frame_location_in_screenshot) else: raise CoordinatesTypeConversionError(from_, to) else: raise CoordinatesTypeConversionError(from_, to) return result
def get_viewport_size(driver): # type: (AnyWebDriver) -> ViewPort argument_guard.not_none(driver) return eyes_selenium_utils.get_viewport_size_or_display_size(driver)
def get_stitched_region(self, region, full_area, position_provider): # type: (Region, Optional[Region], Optional[PositionProvider]) -> Image.Image argument_guard.not_none(region) argument_guard.not_none(position_provider) logger.info( "region: %s ; full_area: %s ; position_provider: %s" % (region, full_area, position_provider.__class__.__name__) ) origin_state = self.origin_provider.get_state() if self.origin_provider != position_provider: self.origin_provider.set_position( Point.ZERO() ) # first scroll to 0,0 so CSS stitching works. # Saving the original position (in case we were already in the outermost frame). original_stitched_state = position_provider.get_state() datetime_utils.sleep(self.wait_before_screenshots) initial_screenshot = self.image_provider.get_image() initial_size = RectangleSize.from_(initial_screenshot) pixel_ratio = self._get_pixel_ratio(initial_screenshot) scaled_cut_provider = self.cut_provider.scale(pixel_ratio) cutted_initial_screenshot = self._cut_if_needed( initial_screenshot, scaled_cut_provider ) self.debug_screenshot_provider.save( cutted_initial_screenshot, self._debug_msg("cutted_initial_screenshot") ) region_in_initial_screenshot = self._get_region_in_screenshot( region, cutted_initial_screenshot, pixel_ratio ) cropped_initial_screenshot = self._crop_if_needed( cutted_initial_screenshot, region_in_initial_screenshot ) self.debug_screenshot_provider.save( cropped_initial_screenshot, self._debug_msg("cropped_initial_screenshot") ) scaled_initial_screenshot = image_utils.scale_image( cropped_initial_screenshot, self.scale_provider ) self.debug_screenshot_provider.save( scaled_initial_screenshot, self._debug_msg("scaled_initial_screenshot") ) if full_area is None or full_area.is_empty: entire_size = self._get_entire_size(initial_screenshot, position_provider) # Notice that this might still happen even if we used # "get_image_part", since "entire_page_size" might be that of a # frame if ( scaled_initial_screenshot.width >= entire_size.width and scaled_initial_screenshot.height >= entire_size.height ): self.origin_provider.restore_state(origin_state) return scaled_initial_screenshot full_area = Region.from_(Point.ZERO(), entire_size) scaled_cropped_location = full_area.location physical_crop_location = Point.from_(scaled_cropped_location).scale(pixel_ratio) if region_in_initial_screenshot.is_empty: physical_crop_size = RectangleSize( initial_size.width - physical_crop_location.x, initial_size.height - physical_crop_location.y, ) source_region = Region.from_(physical_crop_location, physical_crop_size) else: # Starting with the screenshot we already captured at (0,0). source_region = region_in_initial_screenshot scaled_cropped_source_rect = self.cut_provider.to_region(source_region.size) scaled_cropped_source_rect = scaled_cropped_source_rect.offset( source_region.left, source_region.top ) scaled_cropped_source_region = dict( x=int(math.ceil(scaled_cropped_source_rect.left / pixel_ratio)), y=int(math.ceil(scaled_cropped_source_rect.top / pixel_ratio)), width=int(math.ceil(scaled_cropped_source_rect.width / pixel_ratio)), height=int(math.ceil(scaled_cropped_source_rect.height / pixel_ratio)), ) scaled_cropped_size = dict( width=scaled_cropped_source_region["width"], height=scaled_cropped_source_region["height"], ) # Getting the list of viewport regions composing the page # (we'll take screenshot for each one). if region_in_initial_screenshot.is_empty: x = max(0, full_area.left) y = max(0, full_area.top) w = min(full_area.width, scaled_cropped_size["width"]) h = min(full_area.height, scaled_cropped_size["height"]) rect_in_initial_screenshot = Region( round(x * pixel_ratio), round(y * pixel_ratio), round(w * pixel_ratio), round(h * pixel_ratio), ) else: rect_in_initial_screenshot = region_in_initial_screenshot screenshot_parts = self._get_image_parts( full_area, scaled_cropped_size, pixel_ratio, rect_in_initial_screenshot ) # Starting with element region size part of the screenshot. Use it as a size # template. stitched_image = Image.new("RGBA", (full_area.width, full_area.height)) # Take screenshot and stitch for each screenshot part. stitched_image = self._stitch_screenshot( original_stitched_state, position_provider, screenshot_parts, stitched_image, self.scale_provider.scale_ratio, scaled_cut_provider, ) position_provider.restore_state(original_stitched_state) self.origin_provider.restore_state(origin_state) return stitched_image
def _get_viewport_size(self): argument_guard.not_none(self.driver) return eyes_selenium_utils.get_viewport_size(self.driver)
def set_viewport_size(driver, size): # type: (AnyWebDriver, ViewPort) -> None argument_guard.not_none(driver) argument_guard.not_none(size) eyes_selenium_utils.set_viewport_size(driver, size)
def with_batch_id(self, id): # type: (Text) -> BatchInfo argument_guard.not_none(id) self.id = str(id) return self