diff --git a/eyes_selenium/applitools/selenium/capture/eyes_webdriver_screenshot.py b/eyes_selenium/applitools/selenium/capture/eyes_webdriver_screenshot.py index 35b9fcd45..23dea2dbc 100644 --- a/eyes_selenium/applitools/selenium/capture/eyes_webdriver_screenshot.py +++ b/eyes_selenium/applitools/selenium/capture/eyes_webdriver_screenshot.py @@ -1,9 +1,10 @@ from __future__ import absolute_import import typing -from enum import Enum import attr +from selenium.common.exceptions import WebDriverException + from applitools.common import ( CoordinatesType, CoordinatesTypeConversionError, @@ -16,25 +17,21 @@ ) from applitools.common.utils import argument_guard, image_utils from applitools.core.capture import EyesScreenshot, EyesScreenshotFactory -from applitools.selenium import eyes_selenium_utils -from applitools.selenium.positioning import ( - ScrollPositionProvider, - SeleniumPositionProvider, +from applitools.selenium.capture.screenshot_utils import ( + ScreenshotType, + calc_frame_location_in_screenshot, + update_screenshot_type, ) -from selenium.common.exceptions import WebDriverException +from applitools.selenium.eyes_selenium_utils import get_updated_scroll_position +from applitools.selenium.positioning import SeleniumPositionProvider if typing.TYPE_CHECKING: - from typing import Optional, Union + from typing import Union from PIL import Image from applitools.selenium.webdriver import EyesWebDriver from applitools.selenium.frames import FrameChain -class ScreenshotType(Enum): - VIEWPORT = "VIEWPORT" - ENTIRE_FRAME = "ENTIRE_FRAME" - - @attr.s class EyesWebDriverScreenshot(EyesScreenshot): @@ -88,8 +85,8 @@ def from_screenshot(cls, driver, image, screenshot_region): def __attrs_post_init__(self): # type: () -> None self._frame_chain = self._driver.frame_chain.clone() - self._screenshot_type = self.update_screenshot_type( - self._screenshot_type, self._image + self._screenshot_type = update_screenshot_type( + self._screenshot_type, self._image, self._driver ) cur_frame_position_provider = self._driver.eyes.current_frame_position_provider if cur_frame_position_provider: @@ -98,7 +95,7 @@ def __attrs_post_init__(self): position_provider = self._driver.eyes.position_provider if self._current_frame_scroll_position is None: - self._current_frame_scroll_position = self.get_updated_scroll_position( + self._current_frame_scroll_position = get_updated_scroll_position( position_provider ) self._frame_location_in_screenshot = self.get_updated_frame_location_in_screenshot( @@ -123,42 +120,13 @@ def _validate_frame_window(self): def get_updated_frame_location_in_screenshot(self, frame_location_in_screenshot): # type: (Point) -> Point if self.frame_chain.size > 0: - frame_location_in_screenshot = self.calc_frame_location_in_screenshot( + frame_location_in_screenshot = calc_frame_location_in_screenshot( self._driver, self._frame_chain, self._screenshot_type ) elif not frame_location_in_screenshot: frame_location_in_screenshot = Point.zero() return frame_location_in_screenshot - def get_updated_scroll_position(self, position_provider): - # type: (SeleniumPositionProvider) -> Point - try: - sp = position_provider.get_current_position() - if not sp: - sp = Point.zero() - except WebDriverException: - sp = Point.zero() - - return sp - - def update_screenshot_type(self, screenshot_type, image): - # type: ( Optional[ScreenshotType], Image) -> ScreenshotType - if screenshot_type is None: - viewport_size = self._driver.eyes.viewport_size - scale_viewport = self._driver.eyes.stitch_content - - if scale_viewport: - pixel_ratio = self._driver.eyes.device_pixel_ratio - viewport_size = viewport_size.scale(pixel_ratio) - if ( - image.width <= viewport_size["width"] - and image.height <= viewport_size["height"] - ): - screenshot_type = ScreenshotType.VIEWPORT - else: - screenshot_type = ScreenshotType.ENTIRE_FRAME - return screenshot_type - @property def image(self): # type: () -> Union[Image, Image] @@ -180,55 +148,6 @@ def get_frame_size(self, position_provider): frame_size = self._driver.get_default_content_viewport_size() return frame_size - @staticmethod - def _get_default_content_scroll_position(driver): - # type: (EyesWebDriver) -> Point - scroll_root_element = eyes_selenium_utils.current_frame_scroll_root_element( - driver - ) - return ScrollPositionProvider.get_current_position_static( - driver, scroll_root_element - ) - - @staticmethod - def get_default_content_scroll_position(current_frames, driver): - if current_frames.size == 0: - scroll_position = EyesWebDriverScreenshot._get_default_content_scroll_position( - driver - ) - else: - current_fc = driver.eyes._original_frame_chain - with driver.switch_to.frames_and_back(current_fc): - scroll_position = EyesWebDriverScreenshot._get_default_content_scroll_position( - driver - ) - return scroll_position - - @staticmethod - def calc_frame_location_in_screenshot(driver, frame_chain, screenshot_type): - window_scroll = EyesWebDriverScreenshot.get_default_content_scroll_position( - frame_chain, driver - ) - logger.info("Getting first frame...") - first_frame = frame_chain[0] - location_in_screenshot = Point(first_frame.location.x, first_frame.location.y) - # We only need to consider the scroll of the default content if the screenshot is a - # viewport screenshot. If this is a full page screenshot, the frame location will not - # change anyway. - if screenshot_type == ScreenshotType.VIEWPORT: - location_in_screenshot = location_in_screenshot.offset( - -window_scroll.x, -window_scroll.y - ) - - # For inner frames we must calculate the scroll - inner_frames = frame_chain[1:] - for frame in inner_frames: - location_in_screenshot = location_in_screenshot.offset( - frame.location.x - frame.parent_scroll_position.x, - frame.location.y - frame.parent_scroll_position.y, - ) - return location_in_screenshot - @property def frame_chain(self): # type: () -> FrameChain diff --git a/eyes_selenium/applitools/selenium/capture/screenshot_utils.py b/eyes_selenium/applitools/selenium/capture/screenshot_utils.py new file mode 100644 index 000000000..898518787 --- /dev/null +++ b/eyes_selenium/applitools/selenium/capture/screenshot_utils.py @@ -0,0 +1,85 @@ +from enum import Enum +from typing import TYPE_CHECKING, Optional + +from applitools.common import Point, Region, logger +from applitools.common.utils import image_utils +from applitools.selenium import eyes_selenium_utils +from applitools.selenium.eyes_selenium_utils import ( + get_cur_position_provider, + get_updated_scroll_position, +) + +if TYPE_CHECKING: + from PIL import Image + from applitools.selenium.webdriver import EyesWebDriver + + +class ScreenshotType(Enum): + VIEWPORT = "VIEWPORT" + ENTIRE_FRAME = "ENTIRE_FRAME" + + +def update_screenshot_type(screenshot_type, image, driver): + # type: ( Optional[ScreenshotType], Image, EyesWebDriver) -> ScreenshotType + if screenshot_type is None: + viewport_size = driver.eyes.viewport_size + scale_viewport = driver.eyes.stitch_content + + if scale_viewport: + pixel_ratio = driver.eyes.device_pixel_ratio + viewport_size = viewport_size.scale(pixel_ratio) + if ( + image.width <= viewport_size["width"] + and image.height <= viewport_size["height"] + ): + screenshot_type = ScreenshotType.VIEWPORT + else: + screenshot_type = ScreenshotType.ENTIRE_FRAME + return screenshot_type + + +def cut_to_viewport_size_if_required(driver, image): + # type: (EyesWebDriver, Image) -> Image + # Some browsers return always full page screenshot (IE). + # So we cut such images to viewport size + position_provider = get_cur_position_provider(driver) + curr_frame_scroll = get_updated_scroll_position(position_provider) + screenshot_type = update_screenshot_type(None, image, driver) + if screenshot_type != ScreenshotType.VIEWPORT: + viewport_size = driver.eyes.viewport_size + image = image_utils.crop_image( + image, + region_to_crop=Region( + top=curr_frame_scroll.x, + left=0, + height=viewport_size["height"], + width=viewport_size["width"], + ), + ) + return image + + +def calc_frame_location_in_screenshot(driver, frame_chain, screenshot_type): + window_scroll = eyes_selenium_utils.get_default_content_scroll_position( + frame_chain, driver + ) + logger.info("Getting first frame...") + first_frame = frame_chain[0] + location_in_screenshot = Point(first_frame.location.x, first_frame.location.y) + # We only need to consider the scroll of the default content if the screenshot is a + # viewport screenshot. If this is a full page screenshot, the frame location will + # not + # change anyway. + if screenshot_type == ScreenshotType.VIEWPORT: + location_in_screenshot = location_in_screenshot.offset( + -window_scroll.x, -window_scroll.y + ) + + # For inner frames we must calculate the scroll + inner_frames = frame_chain[1:] + for frame in inner_frames: + location_in_screenshot = location_in_screenshot.offset( + frame.location.x - frame.parent_scroll_position.x, + frame.location.y - frame.parent_scroll_position.y, + ) + return location_in_screenshot diff --git a/eyes_selenium/applitools/selenium/eyes_selenium_utils.py b/eyes_selenium/applitools/selenium/eyes_selenium_utils.py index c15988daa..6e12b5d58 100644 --- a/eyes_selenium/applitools/selenium/eyes_selenium_utils.py +++ b/eyes_selenium/applitools/selenium/eyes_selenium_utils.py @@ -12,10 +12,13 @@ from applitools.common import Point, RectangleSize, logger if tp.TYPE_CHECKING: - from typing import Text, Optional, Any, Iterator, Union + from typing import Text, Optional, Any, Union, Iterator + from applitools.selenium.frames import FrameChain + from applitools.selenium.positioning import SeleniumPositionProvider from applitools.selenium.webdriver import EyesWebDriver from applitools.selenium.webelement import EyesWebElement from applitools.selenium.fluent import SeleniumCheckSettings, FrameLocator + from applitools.common.utils.custom_types import ( AnyWebDriver, ViewPort, @@ -35,6 +38,8 @@ "hide_scrollbars", "set_overflow", "parse_location_string", + "get_cur_position_provider", + "get_updated_scroll_position", ) _NATIVE_APP = "NATIVE_APP" @@ -440,7 +445,16 @@ def parse_location_string(position): xy = position.split(";") if len(xy) != 2: raise WebDriverException("Could not get scroll position!") - return Point(float(xy[0]), float(xy[1])) + return Point(round(float(xy[0])), round(float(xy[1]))) + + +def get_current_position(driver, element): + # type: (AnyWebDriver, AnyWebElement) -> Point + element = get_underlying_webelement(element) + xy = driver.execute_script( + "return arguments[0].scrollLeft+';'+arguments[0].scrollTop;", element + ) + return parse_location_string(xy) def scroll_root_element_from(driver, container=None): @@ -469,13 +483,53 @@ def root_html(): return scroll_root_element -def current_frame_scroll_root_element(driver): - # type: (EyesWebDriver) -> EyesWebElement +def current_frame_scroll_root_element(driver, scroll_root_element=None): + # type: (EyesWebDriver, Optional[AnyWebElement]) -> EyesWebElement fc = driver.frame_chain.clone() cur_frame = fc.peek root_element = None if cur_frame: root_element = cur_frame.scroll_root_element if root_element is None and not driver.is_mobile_app: - root_element = driver.find_element_by_tag_name("html") + if scroll_root_element: + root_element = scroll_root_element + else: + root_element = driver.find_element_by_tag_name("html") return root_element + + +def get_cur_position_provider(driver): + # type: (EyesWebDriver) -> SeleniumPositionProvider + cur_frame_position_provider = driver.eyes.current_frame_position_provider + if cur_frame_position_provider: + return cur_frame_position_provider + else: + return driver.eyes.position_provider + + +def get_updated_scroll_position(position_provider): + # type: (SeleniumPositionProvider) -> Point + try: + sp = position_provider.get_current_position() + if not sp: + sp = Point.zero() + except WebDriverException: + sp = Point.zero() + + return sp + + +def get_default_content_scroll_position(current_frames, driver): + # type: (FrameChain, EyesWebDriver) -> Point + if current_frames.size == 0: + + scroll_position = get_current_position( + driver, current_frame_scroll_root_element(driver) + ) + else: + current_fc = driver.eyes.original_frame_chain + with driver.switch_to.frames_and_back(current_fc): + scroll_position = get_current_position( + driver, current_frame_scroll_root_element(driver) + ) + return scroll_position diff --git a/eyes_selenium/applitools/selenium/selenium_eyes.py b/eyes_selenium/applitools/selenium/selenium_eyes.py index 3d2986ce8..1142f12bf 100644 --- a/eyes_selenium/applitools/selenium/selenium_eyes.py +++ b/eyes_selenium/applitools/selenium/selenium_eyes.py @@ -3,7 +3,6 @@ from time import sleep from selenium.common.exceptions import WebDriverException -from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver from applitools.common import ( AppEnvironment, @@ -23,11 +22,11 @@ FixedScaleProvider, ImageProvider, MouseTrigger, + NullCutProvider, NullScaleProvider, PositionProvider, RegionProvider, TextTrigger, - NullCutProvider, ) from applitools.selenium.capture.eyes_webdriver_screenshot import ( EyesWebDriverScreenshotFactory, @@ -36,6 +35,9 @@ FullPageCaptureAlgorithm, ) from applitools.selenium.capture.image_providers import get_image_provider +from applitools.selenium.capture.screenshot_utils import ( + cut_to_viewport_size_if_required, +) from applitools.selenium.region_compensation import ( RegionPositionCompensation, get_region_position_compensation, @@ -95,7 +97,7 @@ class SeleniumEyes(EyesBase): _user_agent = None # type: Optional[UserAgent] _image_provider = None # type: Optional[ImageProvider] _region_position_compensation = None # type: Optional[RegionPositionCompensation] - + _is_check_region = None # type: Optional[bool] current_frame_position_provider = None # type: Optional[PositionProvider] @staticmethod @@ -753,24 +755,28 @@ def _viewport_screenshot(self, scale_provider): # type: (ScaleProvider) -> EyesWebDriverScreenshot logger.info("Viewport screenshot requested") self._ensure_element_visible(self._target_element) - sleep(self.configuration.wait_before_screenshots / 1000.0) + image = self._get_scaled_cropped_image(scale_provider) + if not self._is_check_region and not self._driver.is_mobile_app: + # Some browsers return always full page screenshot (IE). + # So we cut such images to viewport size + image = cut_to_viewport_size_if_required(self.driver, image) + return EyesWebDriverScreenshot.create_viewport(self._driver, image) + + def _get_scaled_cropped_image(self, scale_provider): image = self._image_provider.get_image() self._debug_screenshot_provider.save(image, "original") - scale_provider.update_scale_ratio(image.width) pixel_ratio = 1 / scale_provider.scale_ratio if pixel_ratio != 1.0: - logger.info("Scalling") + logger.info("Scaling") image = image_utils.scale_image(image, 1.0 / pixel_ratio) self._debug_screenshot_provider.save(image, "scaled") - if not isinstance(self.cut_provider, NullCutProvider): logger.info("Cutting") image = self.cut_provider.cut(image) self._debug_screenshot_provider.save(image, "cutted") - - return EyesWebDriverScreenshot.create_viewport(self._driver, image) + return image def _get_viewport_scroll_bounds(self): switch_to = self.driver.switch_to @@ -889,6 +895,8 @@ def _check_element(self, name, check_settings): return result def _check_region(self, name, check_settings): + self._is_check_region = True + def get_region(): location = self._target_element.location size = self._target_element.size @@ -903,4 +911,5 @@ def get_region(): result = self._check_window_base( RegionProvider(get_region), name, False, check_settings ) + self._is_check_region = False return result diff --git a/tests/functional/eyes_selenium/selenium/test_specific_cases.py b/tests/functional/eyes_selenium/selenium/test_specific_cases.py index 55f0f2c56..e7e85ade4 100644 --- a/tests/functional/eyes_selenium/selenium/test_specific_cases.py +++ b/tests/functional/eyes_selenium/selenium/test_specific_cases.py @@ -1,3 +1,5 @@ +import os + import pytest from applitools.selenium import Region, Target @@ -59,3 +61,22 @@ def test_abort_eyes(eyes, driver): eyes.open(driver, "Python VisualGrid", "TestAbortSeleniumEyes") eyes.check_window() eyes.abort() + + +def test_ie_viewport_screenshot(eyes, webdriver_module): + sauce_url = "https://{username}:{password}@ondemand.saucelabs.com:443/wd/hub".format( + username=os.getenv("SAUCE_USERNAME", None), + password=os.getenv("SAUCE_ACCESS_KEY", None), + ) + driver = webdriver_module.Remote( + command_executor=sauce_url, + desired_capabilities={ + "browserName": "internet explorer", + "platform": "Windows 10", + "version": "11.285", + }, + ) + driver.get("http://applitools.github.io/demo/TestPages/FramesTestPage") + eyes.open(driver, "Python SDK", "TestIEViewportScreenshot") + eyes.check_window() + eyes.close()