diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md index 9640e93a..c1ad66a9 100644 --- a/docs/source/getting_started.md +++ b/docs/source/getting_started.md @@ -160,7 +160,7 @@ def test_example(driver_wrapper): forms_page.controls_section.python_checkbox.click() - assert forms_page.controls_section.checkbox_label == 'PYTHON' + assert forms_page.controls_section.checkbox_label.text == 'PYTHON' ``` --- diff --git a/mops/__init__.py b/mops/__init__.py index 97b3b9fc..95cc3fae 100644 --- a/mops/__init__.py +++ b/mops/__init__.py @@ -1,2 +1,2 @@ -__version__ = '3.1.0' +__version__ = '3.2.0' __project_name__ = 'mops' diff --git a/mops/abstraction/driver_wrapper_abc.py b/mops/abstraction/driver_wrapper_abc.py index b6c8b3b6..46b80388 100644 --- a/mops/abstraction/driver_wrapper_abc.py +++ b/mops/abstraction/driver_wrapper_abc.py @@ -2,20 +2,19 @@ from abc import ABC from functools import cached_property -from typing import List, Union, Any, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, List, NoReturn, Optional, Tuple, Union -from playwright.sync_api import Page as PlaywrightPage - -from mops.mixins.objects.box import Box -from selenium.webdriver.common.alert import Alert -from PIL import Image - -from mops.mixins.objects.size import Size from mops.utils.internal_utils import WAIT_EL, WAIT_UNIT if TYPE_CHECKING: + from PIL import Image + from playwright.sync_api import Page as PlaywrightPage + from selenium.webdriver.common.alert import Alert + from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions from mops.base.element import Element + from mops.mixins.objects.box import Box + from mops.mixins.objects.size import Size class DriverWrapperABC(ABC): @@ -54,7 +53,7 @@ def is_safari(self) -> bool: :return: :obj:`bool`- :obj:`True` if the current driver is Safari, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError @cached_property def is_chrome(self) -> bool: @@ -63,7 +62,7 @@ def is_chrome(self) -> bool: :return: :obj:`bool`- :obj:`True` if the current driver is Chrome, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError @cached_property def is_firefox(self) -> bool: @@ -72,9 +71,9 @@ def is_firefox(self) -> bool: :return: :obj:`bool`- :obj:`True` if the current driver is Firefox, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError - def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): + def quit(self, silent: bool = False, trace_path: str = 'trace.zip') -> NoReturn: """ Quit the driver instance. @@ -93,9 +92,9 @@ def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): :return: :obj:`None` """ - raise NotImplementedError() + raise NotImplementedError - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> DriverWrapper: + def wait(self, timeout: float = WAIT_UNIT) -> DriverWrapper: """ Pauses the execution for a specified amount of time. @@ -104,7 +103,7 @@ def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def get(self, url: str, silent: bool = False) -> DriverWrapper: """ @@ -116,7 +115,7 @@ def get(self, url: str, silent: bool = False) -> DriverWrapper: :type silent: bool :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def is_driver_opened(self) -> bool: """ @@ -124,7 +123,7 @@ def is_driver_opened(self) -> bool: :return: :obj:`bool` - :obj:`True` if the driver is open, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_driver_closed(self) -> bool: """ @@ -132,7 +131,7 @@ def is_driver_closed(self) -> bool: :return: :obj:`bool` - :obj:`True` if the driver is closed, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError @property def current_url(self) -> str: @@ -141,7 +140,7 @@ def current_url(self) -> str: :return: :obj:`str` - The URL of the current page. """ - raise NotImplementedError() + raise NotImplementedError def refresh(self) -> DriverWrapper: """ @@ -149,7 +148,7 @@ def refresh(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def go_forward(self) -> DriverWrapper: """ @@ -157,7 +156,7 @@ def go_forward(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def go_back(self) -> DriverWrapper: """ @@ -165,7 +164,7 @@ def go_back(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def set_cookie(self, cookies: List[dict]) -> DriverWrapper: """ @@ -177,7 +176,7 @@ def set_cookie(self, cookies: List[dict]) -> DriverWrapper: :type cookies: typing.List[dict] :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def clear_cookies(self) -> DriverWrapper: """ @@ -185,7 +184,7 @@ def clear_cookies(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def delete_cookie(self, name: str) -> DriverWrapper: """ @@ -193,7 +192,7 @@ def delete_cookie(self, name: str) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def get_cookies(self) -> List[dict]: """ @@ -202,7 +201,7 @@ def get_cookies(self) -> List[dict]: :return: A list of dictionaries, each containing cookie data. :rtype: typing.List[typing.Dict] """ - raise NotImplementedError() + raise NotImplementedError def switch_to_frame(self, frame: Element) -> DriverWrapper: """ @@ -212,7 +211,7 @@ def switch_to_frame(self, frame: Element) -> DriverWrapper: :type frame: Element :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def switch_to_default_content(self) -> DriverWrapper: """ @@ -220,7 +219,7 @@ def switch_to_default_content(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def execute_script(self, script: str, *args) -> Any: """ @@ -233,7 +232,7 @@ def execute_script(self, script: str, *args) -> Any: :type args: list :return: :obj:`typing.Any` - The result of the JavaScript execution. """ - raise NotImplementedError() + raise NotImplementedError def evaluate(self, expression: str, arg: Any = None) -> Any: """ @@ -245,7 +244,7 @@ def evaluate(self, expression: str, arg: Any = None) -> Any: :type arg: list :return: :obj:`typing.Any` - The result of the JavaScript execution. """ - raise NotImplementedError() + raise NotImplementedError def set_page_load_timeout(self, timeout: int = 30) -> DriverWrapper: """ @@ -255,7 +254,7 @@ def set_page_load_timeout(self, timeout: int = 30) -> DriverWrapper: :type timeout: int :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def set_window_size(self, size: Size) -> DriverWrapper: """ @@ -264,7 +263,7 @@ def set_window_size(self, size: Size) -> DriverWrapper: :param size: The desired inner window size as a :class:`.Size` object. :return: The current instance of :obj:`.DriverWrapper`. """ - raise NotImplementedError() + raise NotImplementedError def get_inner_window_size(self) -> Size: """ @@ -272,7 +271,7 @@ def get_inner_window_size(self) -> Size: :return: The size of the inner window as a :class:`.Size` object. """ - raise NotImplementedError() + raise NotImplementedError def get_window_size(self) -> Size: """ @@ -286,13 +285,13 @@ def get_window_size(self) -> Size: :return: The size of the outer window as a :class:`.Size` object. """ - raise NotImplementedError() + raise NotImplementedError def save_screenshot( self, file_name: str, screenshot_base: Union[Image, bytes] = None, - convert_type: str = None + convert_type: Optional[str] = None, ) -> Image: """ Takes a full screenshot of the driver and saves it to the specified path/filename. @@ -305,15 +304,15 @@ def save_screenshot( :type convert_type: str :return: :class:`PIL.Image.Image` """ - raise NotImplementedError() + raise NotImplementedError def assert_screenshot( self, filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, remove: Union[Element, List[Element]] = None, cut_box: Box = None, hide: Union[Element, List[Element]] = None, @@ -347,15 +346,15 @@ def assert_screenshot( :type hide: typing.Optional[Element or typing.List[Element]] :return: :obj:`None` """ - raise NotImplementedError() + raise NotImplementedError def soft_assert_screenshot( self, filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, remove: Union[Element, List[Element]] = None, cut_box: Box = None, hide: Union[Element, List[Element]] = None, @@ -387,19 +386,19 @@ def soft_assert_screenshot( Can be a single element or a list of elements. :return: :class:`typing.Tuple` (:class:`bool`, :class:`str`) - result state and result message """ - raise NotImplementedError() + raise NotImplementedError - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web page. - Appium iOS: Removes native controls from image manually + Appium iOS: Removes native controls from image manually. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot :type screenshot_base: bytes :return: :class:`PIL.Image.Image` """ - raise NotImplementedError() + raise NotImplementedError @property def screenshot_base(self) -> bytes: @@ -408,7 +407,7 @@ def screenshot_base(self) -> bytes: :return: :class:`bytes` - screenshot binary """ - raise NotImplementedError() + raise NotImplementedError def get_all_tabs(self) -> List[str]: """ @@ -417,7 +416,7 @@ def get_all_tabs(self) -> List[str]: :return: A list of :class:`str`, each representing an open tab. :rtype: typing.List[str] """ - raise NotImplementedError() + raise NotImplementedError def create_new_tab(self) -> DriverWrapper: """ @@ -425,7 +424,7 @@ def create_new_tab(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, now switched to the new tab. """ - raise NotImplementedError() + raise NotImplementedError def switch_to_original_tab(self) -> DriverWrapper: """ @@ -433,7 +432,7 @@ def switch_to_original_tab(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, now switched to the original tab. """ - raise NotImplementedError() + raise NotImplementedError def switch_to_tab(self, tab: int = -1) -> DriverWrapper: """ @@ -443,7 +442,7 @@ def switch_to_tab(self, tab: int = -1) -> DriverWrapper: :type tab: int :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, now switched to the specified tab. """ - raise NotImplementedError() + raise NotImplementedError def close_unused_tabs(self) -> DriverWrapper: """ @@ -452,7 +451,7 @@ def close_unused_tabs(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, with all tabs except the original closed. """ - raise NotImplementedError() + raise NotImplementedError def click_by_coordinates(self, x: int, y: int, silent: bool = False) -> DriverWrapper: """ @@ -466,7 +465,7 @@ def click_by_coordinates(self, x: int, y: int, silent: bool = False) -> DriverWr :type silent: bool :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def is_app_installed(self) -> bool: """ @@ -474,7 +473,7 @@ def is_app_installed(self) -> bool: :return: :obj:`bool` - :obj:`True` if the app is running, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_app_deleted(self) -> bool: """ @@ -482,7 +481,7 @@ def is_app_deleted(self) -> bool: :return: :obj:`bool` - :obj:`True` if the app is deleted, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_app_closed(self) -> bool: """ @@ -490,7 +489,7 @@ def is_app_closed(self) -> bool: :return: :obj:`bool` - :obj:`True` if the app is closed, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_app_in_foreground(self) -> bool: """ @@ -498,7 +497,7 @@ def is_app_in_foreground(self) -> bool: :return: :obj:`bool` - :obj:`True` if the app is in the foreground, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_app_in_background(self) -> bool: """ @@ -506,7 +505,7 @@ def is_app_in_background(self) -> bool: :return: :obj:`bool` - :obj:`True` if the app is in the background, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def terminate_app(self, bundle_id: str) -> bool: """ @@ -516,7 +515,7 @@ def terminate_app(self, bundle_id: str) -> bool: :type bundle_id: str :return: :obj:`bool` - :obj:`True` if the app has been successfully terminated, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def switch_to_native(self) -> DriverWrapper: """ @@ -524,7 +523,7 @@ def switch_to_native(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, now in the native app context. """ - raise NotImplementedError() + raise NotImplementedError def switch_to_web(self) -> DriverWrapper: """ @@ -532,7 +531,7 @@ def switch_to_web(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper, now in the web app context. """ - raise NotImplementedError() + raise NotImplementedError def get_web_view_context(self) -> Union[None, str]: """ @@ -541,7 +540,7 @@ def get_web_view_context(self) -> Union[None, str]: :return: :obj:`None` if no WEBVIEW context is found, otherwise the name of the WEBVIEW context. :rtype: typing.Union[None, str] """ - raise NotImplementedError() + raise NotImplementedError def get_current_context(self) -> str: """ @@ -549,7 +548,7 @@ def get_current_context(self) -> str: :return: :class:`str` - The name of the current context. """ - raise NotImplementedError() + raise NotImplementedError def is_native_context(self) -> bool: """ @@ -557,7 +556,7 @@ def is_native_context(self) -> bool: :return: :obj:`bool` - :obj:`True` if the current context is native, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def is_web_context(self) -> bool: """ @@ -565,7 +564,7 @@ def is_web_context(self) -> bool: :return: :obj:`bool` - :obj:`True` if the current context is web, otherwise :obj:`False`. """ - raise NotImplementedError() + raise NotImplementedError def get_all_contexts(self) -> List[str]: """ @@ -574,7 +573,7 @@ def get_all_contexts(self) -> List[str]: :return: A list of available context names. :rtype: typing.List[str] """ - raise NotImplementedError() + raise NotImplementedError def hide_keyboard(self, **kwargs) -> DriverWrapper: """ @@ -583,27 +582,27 @@ def hide_keyboard(self, **kwargs) -> DriverWrapper: :param kwargs: Additional arguments passed to the `Keyboard.hide_keyboard` method. :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError @property def top_bar_height(self) -> int: """ - iOS only - Get the height of the top bar. + IOS only - Get the height of the top bar. :return: :obj:`int` - The height of the top bar in pixels. """ - raise NotImplementedError() + raise NotImplementedError @property def bottom_bar_height(self) -> int: """ - iOS only - Get the height of the bottom bar. + IOS only - Get the height of the bottom bar. :return: :obj:`int` - The height of the bottom bar in pixels. """ - raise NotImplementedError() + raise NotImplementedError - def switch_to_alert(self, timeout: Union[int, float] = WAIT_EL) -> Alert: + def switch_to_alert(self, timeout: float = WAIT_EL) -> Alert: """ Appium/Selenium only: Wait for an alert and switch to it. @@ -611,7 +610,7 @@ def switch_to_alert(self, timeout: Union[int, float] = WAIT_EL) -> Alert: :type timeout: Union[int, float] :return: :obj:`selenium.webdriver.common.alert.Alert` - The alert object. """ - raise NotImplementedError() + raise NotImplementedError def accept_alert(self) -> DriverWrapper: """ @@ -619,7 +618,7 @@ def accept_alert(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError def dismiss_alert(self) -> DriverWrapper: """ @@ -627,6 +626,6 @@ def dismiss_alert(self) -> DriverWrapper: :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ - raise NotImplementedError() + raise NotImplementedError diff --git a/mops/abstraction/element_abc.py b/mops/abstraction/element_abc.py index 8c8a5b24..02c14c3f 100644 --- a/mops/abstraction/element_abc.py +++ b/mops/abstraction/element_abc.py @@ -1,25 +1,24 @@ from __future__ import annotations from abc import ABC -from typing import Union, Any, List, Tuple, Optional, TYPE_CHECKING - -from PIL.Image import Image -from appium.webdriver.extensions.location import Location - -from mops.mixins.objects.box import Box -from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes -from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement -from appium.webdriver.webelement import WebElement as AppiumWebElement -from playwright.sync_api import Locator as PlayWebElement +from typing import TYPE_CHECKING, Any, List, NoReturn, Optional, Tuple, Union from mops.abstraction.mixin_abc import MixinABC -from mops.keyboard_keys import KeyboardKeys -from mops.mixins.objects.size import Size -from mops.utils.internal_utils import WAIT_EL, QUARTER_WAIT_EL +from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes +from mops.utils.internal_utils import QUARTER_WAIT_EL, WAIT_EL if TYPE_CHECKING: - from mops.mixins.objects.locator import Locator + from appium.webdriver.extensions.location import Location + from appium.webdriver.webelement import WebElement as AppiumWebElement + from PIL.Image import Image + from playwright.sync_api import Locator as PlayWebElement + from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement + from mops.base.element import Element + from mops.keyboard_keys import KeyboardKeys + from mops.mixins.objects.box import Box + from mops.mixins.objects.locator import Locator + from mops.mixins.objects.size import Size class ElementABC(MixinABC, ABC): @@ -31,25 +30,25 @@ class ElementABC(MixinABC, ABC): @property def element(self) -> Union[SeleniumWebElement, AppiumWebElement, PlayWebElement]: - """ + r""" Returns a source element object, depending on the current driver in use. :return: :class:`selenium.webdriver.remote.webelement.WebElement` or\n :class:`appium.webdriver.webelement.WebElement` or\n :class:`playwright.sync_api.Locator` """ - raise NotImplementedError() + raise NotImplementedError @element.setter - def element(self, base_element: Union[SeleniumWebElement, AppiumWebElement, PlayWebElement]): - """ + def element(self, base_element: Union[SeleniumWebElement, AppiumWebElement, PlayWebElement]) -> NoReturn: + r""" Sets the source element object. :param base_element: :class:`selenium.webdriver.remote.webelement.WebElement` or\n :class:`appium.webdriver.webelement.WebElement` or\n :class:`playwright.sync_api.Locator` """ - raise NotImplementedError() + raise NotImplementedError @property def all_elements(self) -> Union[List[Element], List[Any]]: @@ -58,7 +57,7 @@ def all_elements(self) -> Union[List[Element], List[Any]]: :return: A list of wrapped :class:`Element` objects. """ - raise NotImplementedError() + raise NotImplementedError def click(self, *, force_wait: bool = True, **kwargs) -> Element: """ @@ -79,7 +78,7 @@ def click(self, *, force_wait: bool = True, **kwargs) -> Element: :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def click_into_center(self, silent: bool = False) -> Element: """ @@ -89,7 +88,7 @@ def click_into_center(self, silent: bool = False) -> Element: :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def type_text(self, text: Union[str, KeyboardKeys], silent: bool = False) -> Element: """ @@ -101,7 +100,7 @@ def type_text(self, text: Union[str, KeyboardKeys], silent: bool = False) -> Ele :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def type_slowly(self, text: str, sleep_gap: float = 0.05, silent: bool = False) -> Element: """ @@ -115,7 +114,7 @@ def type_slowly(self, text: str, sleep_gap: float = 0.05, silent: bool = False) :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def clear_text(self, silent: bool = False) -> Element: """ @@ -125,7 +124,7 @@ def clear_text(self, silent: bool = False) -> Element: :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def check(self) -> Element: """ @@ -133,7 +132,7 @@ def check(self) -> Element: :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def uncheck(self) -> Element: """ @@ -141,12 +140,21 @@ def uncheck(self) -> Element: :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError - def wait_visibility(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Element: + def wait_visibility( + self, + *, + timeout: int = WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, + ) -> Element: """ Waits until the element becomes visible. - **Note:** The method requires the use of named arguments. + **Note:** The method requires the use of named arguments. + + A continuous visibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium:** @@ -163,14 +171,26 @@ def wait_visibility(self, *, timeout: int = WAIT_EL, silent: bool = False) -> El :type timeout: int :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous visibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError - def wait_hidden(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Element: + def wait_hidden( + self, + *, + timeout: int = WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, + ) -> Element: """ Waits until the element becomes hidden. - **Note:** The method requires the use of named arguments. + **Note:** The method requires the use of named arguments. + + A continuous invisibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium:** @@ -187,14 +207,17 @@ def wait_hidden(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Elemen :type timeout: int :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous invisibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Element: - """ + r""" Waits until the element becomes available in DOM tree. \n - **Note:** The method requires the use of named arguments. + **Note:** The method requires the use of named arguments. **Selenium:** @@ -213,13 +236,13 @@ def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def save_screenshot( self, file_name: str, screenshot_base: Union[bytes, Image] = None, - convert_type: str = None + convert_type: Optional[str] = None, ) -> Image: """ Saves a screenshot of the element. @@ -232,7 +255,7 @@ def save_screenshot( :type convert_type: str :return: :class:`PIL.Image.Image` """ - raise NotImplementedError() + raise NotImplementedError def hide(self) -> Element: """ @@ -240,7 +263,7 @@ def hide(self) -> Element: :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def execute_script(self, script: str, *args) -> Any: """ @@ -252,19 +275,19 @@ def execute_script(self, script: str, *args) -> Any: that appear in script as ``arguments[1]`` ``arguments[2]`` etc. :return: :obj:`typing.Any` result from the script. """ - raise NotImplementedError() + raise NotImplementedError - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. - Appium iOS: Take driver screenshot and crop manually element from it + Appium iOS: Take driver screenshot and crop manually element from it. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot :type screenshot_base: bytes :return: :class:`PIL.Image.Image` """ - raise NotImplementedError() + raise NotImplementedError @property def screenshot_base(self) -> bytes: @@ -273,7 +296,7 @@ def screenshot_base(self) -> bytes: :return: :class:`bytes` - screenshot binary """ - raise NotImplementedError() + raise NotImplementedError @property def text(self) -> str: @@ -282,7 +305,7 @@ def text(self) -> str: :return: :class:`str` - element text """ - raise NotImplementedError() + raise NotImplementedError @property def inner_text(self) -> str: @@ -291,7 +314,7 @@ def inner_text(self) -> str: :return: :class:`str` - element inner text """ - raise NotImplementedError() + raise NotImplementedError @property def value(self) -> str: @@ -300,7 +323,7 @@ def value(self) -> str: :return: :class:`str` - element value """ - raise NotImplementedError() + raise NotImplementedError def is_available(self) -> bool: """ @@ -308,7 +331,7 @@ def is_available(self) -> bool: :return: :class:`bool` - :obj:`True` if present in DOM """ - raise NotImplementedError() + raise NotImplementedError def is_displayed(self, silent: bool = False) -> bool: """ @@ -318,7 +341,7 @@ def is_displayed(self, silent: bool = False) -> bool: :type silent: bool :return: :class:`bool` """ - raise NotImplementedError() + raise NotImplementedError def is_hidden(self, silent: bool = False) -> bool: """ @@ -328,7 +351,7 @@ def is_hidden(self, silent: bool = False) -> bool: :type silent: bool :return: :class:`bool` """ - raise NotImplementedError() + raise NotImplementedError def get_attribute(self, attribute: str, silent: bool = False) -> str: """ @@ -340,7 +363,7 @@ def get_attribute(self, attribute: str, silent: bool = False) -> str: :type silent: bool :return: :class:`str` - The value of the specified attribute. """ - raise NotImplementedError() + raise NotImplementedError def get_all_texts(self, silent: bool = False) -> List[str]: """ @@ -350,7 +373,7 @@ def get_all_texts(self, silent: bool = False) -> List[str]: :type silent: bool :return: :class:`list` of :class:`str` - A list containing the text content of all matching elements. """ - raise NotImplementedError() + raise NotImplementedError def get_elements_count(self, silent: bool = False) -> int: """ @@ -360,7 +383,7 @@ def get_elements_count(self, silent: bool = False) -> int: :type silent: bool :return: :class:`int` - The number of matching elements. """ - raise NotImplementedError() + raise NotImplementedError def get_rect(self) -> dict: """ @@ -368,7 +391,7 @@ def get_rect(self) -> dict: :return: :class:`dict` - A dictionary {'x', 'y', 'width', 'height'} of the element. """ - raise NotImplementedError() + raise NotImplementedError @property def size(self) -> Size: @@ -377,7 +400,7 @@ def size(self) -> Size: :return: :class:`.Size` - An object representing the element's dimensions. """ - raise NotImplementedError() + raise NotImplementedError @property def location(self) -> Location: @@ -386,7 +409,7 @@ def location(self) -> Location: :return: :class:`.Location` - An object representing the element's position. """ - raise NotImplementedError() + raise NotImplementedError def is_enabled(self, silent: bool = False) -> bool: """ @@ -396,7 +419,7 @@ def is_enabled(self, silent: bool = False) -> bool: :type silent: bool :return: :class:`bool` - :obj:`True` if the element is enabled, :obj:`False` otherwise. """ - raise NotImplementedError() + raise NotImplementedError def is_checked(self) -> bool: """ @@ -404,7 +427,7 @@ def is_checked(self) -> bool: :return: :class:`bool` - :obj:`True` if the checkbox or radio button is checked, :obj:`False` otherwise. """ - raise NotImplementedError() + raise NotImplementedError def hover(self, silent: bool = False) -> Element: """ @@ -414,7 +437,7 @@ def hover(self, silent: bool = False) -> Element: :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def hover_outside(self, x: int = 0, y: int = -5) -> Element: """ @@ -426,7 +449,7 @@ def hover_outside(self, x: int = 0, y: int = -5) -> Element: :type y: int :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def click_outside(self, x: int = -5, y: int = -5) -> Element: """ @@ -438,7 +461,7 @@ def click_outside(self, x: int = -5, y: int = -5) -> Element: :type y: int :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def click_in_alert(self) -> Element: """ @@ -447,7 +470,7 @@ def click_in_alert(self) -> Element: :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def set_text(self, text: str, silent: bool = False) -> Element: """ @@ -459,7 +482,7 @@ def set_text(self, text: str, silent: bool = False) -> Element: :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def send_keyboard_action(self, action: Union[str, KeyboardKeys]) -> Element: """ @@ -469,14 +492,14 @@ def send_keyboard_action(self, action: Union[str, KeyboardKeys]) -> Element: :type action: str or :class:`KeyboardKeys` :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_elements_count( self, expected_count: int, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait until the number of matching elements equals the expected count. @@ -502,14 +525,14 @@ def wait_elements_count( :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_for_text( self, expected_text: Optional[str] = None, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait for the presence of a specific text in the current element, or for any non-empty text. @@ -535,14 +558,14 @@ def wait_for_text( :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_for_value( self, expected_value: Optional[str] = None, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait for a specific value in the current element, or for any non-empty value. @@ -568,18 +591,21 @@ def wait_for_value( :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_visibility_without_error( self, *, - timeout: Union[int, float] = QUARTER_WAIT_EL, - silent: bool = False + timeout: float = QUARTER_WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, ) -> Element: """ Wait for the element to become visible, without raising an error if it does not. + **Note:** The method requires the use of named arguments. - **Note:** The method requires the use of named arguments. + A continuous visibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium & Playwright:** @@ -596,20 +622,26 @@ def wait_visibility_without_error( :type timeout: typing.Union[int, float] :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous visibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_hidden_without_error( self, *, - timeout: Union[int, float] = QUARTER_WAIT_EL, - silent: bool = False + timeout: float = QUARTER_WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, ) -> Element: """ Wait for the element to become hidden, without raising an error if it does not. + **Note:** The method requires the use of named arguments. - **Note:** The method requires the use of named arguments. + A continuous invisibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium & Playwright:** @@ -626,11 +658,14 @@ def wait_hidden_without_error( :type timeout: typing.Union[int, float] :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous invisibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError - def wait_enabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = False) -> Element: + def wait_enabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element: """ Wait for the element to become enabled and/or clickable. @@ -653,9 +688,9 @@ def wait_enabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = F :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError - def wait_disabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = False) -> Element: + def wait_disabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element: """ Wait for the element to become disabled. @@ -678,17 +713,17 @@ def wait_disabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def wait_for_size( self, expected_size: Size, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ - Wait until element size will be equal to given :class:`.Size` object + Wait until element size will be equal to given :class:`.Size` object. **Note:** The method requires the use of named arguments except ``expected_size``. @@ -711,7 +746,7 @@ def wait_for_size( :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def is_visible(self, check_displaying: bool = True, silent: bool = False) -> bool: """ @@ -724,11 +759,11 @@ def is_visible(self, check_displaying: bool = True, silent: bool = False) -> boo :type silent: bool :return: :class:`bool` """ - raise NotImplementedError() + raise NotImplementedError def is_fully_visible(self, check_displaying: bool = True, silent: bool = False) -> bool: """ - Check is current element top left corner and bottom right corner visible on current screen + Check is current element top left corner and bottom right corner visible on current screen. :param check_displaying: If :obj:`True`, the :func:`is_displayed` method will be called to further verify visibility. The check will stop if this method returns :obj:`False`. @@ -737,13 +772,13 @@ def is_fully_visible(self, check_displaying: bool = True, silent: bool = False) :type silent: bool :return: :class:`bool` """ - raise NotImplementedError() + raise NotImplementedError def scroll_into_view( self, block: ScrollTo = ScrollTo.CENTER, behavior: ScrollTypes = ScrollTypes.INSTANT, - sleep: Union[int, float] = 0, + sleep: float = 0, silent: bool = False, ) -> Element: """ @@ -759,15 +794,15 @@ def scroll_into_view( :type silent: bool :return: :class:`Element` """ - raise NotImplementedError() + raise NotImplementedError def assert_screenshot( self, filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, scroll: bool = False, remove: Union[Element, List[Element]] = None, fill_background: Union[str, bool] = False, @@ -808,15 +843,15 @@ def assert_screenshot( :type hide: typing.Optional[Element or typing.List[Element]] :return: :obj:`None` """ - raise NotImplementedError() + raise NotImplementedError def soft_assert_screenshot( self, filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, scroll: bool = False, remove: Union[Element, List[Element]] = None, fill_background: Union[str, bool] = False, @@ -855,7 +890,7 @@ def soft_assert_screenshot( Can be a single element or a list of elements. :return: :class:`typing.Tuple` (:class:`bool`, :class:`str`) - result state and result message """ - raise NotImplementedError() + raise NotImplementedError def get_element_info(self, element: Optional[Element] = None) -> str: """ @@ -866,7 +901,7 @@ def get_element_info(self, element: Optional[Element] = None) -> str: :type element: :class:`Element` or :obj:`None` :return: :class:`str` - A string containing the log data. """ - raise NotImplementedError() + raise NotImplementedError def _get_all_elements(self, sources: Union[tuple, list]) -> List[Element]: """ @@ -876,4 +911,4 @@ def _get_all_elements(self, sources: Union[tuple, list]) -> List[Element]: :type sources: tuple or list :return: A list of wrapped :class:`Element` objects. """ - raise NotImplementedError() + raise NotImplementedError diff --git a/mops/abstraction/mixin_abc.py b/mops/abstraction/mixin_abc.py index 8531ae94..6f387b08 100644 --- a/mops/abstraction/mixin_abc.py +++ b/mops/abstraction/mixin_abc.py @@ -1,19 +1,19 @@ from abc import ABC -from typing import Union, Any +from typing import Any, NoReturn, Union from appium.webdriver.webdriver import WebDriver as AppiumWebDriver -from mops.utils.logs import LogLevel from playwright.sync_api import Page as PlaywrightSourcePage from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver from mops.base.driver_wrapper import DriverWrapper +from mops.utils.logs import LogLevel class MixinABC(ABC): @property def driver(self) -> Union[SeleniumWebDriver, AppiumWebDriver, PlaywrightSourcePage]: - """ + r""" Retrieves the source driver instance, which could be a Selenium, Appium, or Playwright driver. :return: Current source driver that assigned for this object, which is either \n @@ -21,7 +21,7 @@ def driver(self) -> Union[SeleniumWebDriver, AppiumWebDriver, PlaywrightSourcePa :class:`appium.webdriver.webdriver.WebDriver` or\n :class:`playwright.sync_api.Page` instance. """ - raise NotImplementedError() + raise NotImplementedError @property def driver_wrapper(self) -> DriverWrapper: @@ -31,17 +31,17 @@ def driver_wrapper(self) -> DriverWrapper: :return: The current :obj:`.DriverWrapper` instance that assigned for this object. :rtype: DriverWrapper """ - raise NotImplementedError() + raise NotImplementedError @driver_wrapper.setter - def driver_wrapper(self, driver_wrapper: DriverWrapper): + def driver_wrapper(self, driver_wrapper: DriverWrapper) -> NoReturn: """ Sets the driver wrapper instance, for this object. :param driver_wrapper: The DriverWrapper instance to be set. :type driver_wrapper: DriverWrapper """ - raise NotImplementedError() + raise NotImplementedError def log(self: Any, message: str, level: str = LogLevel.INFO) -> None: """ @@ -60,4 +60,4 @@ def log(self: Any, message: str, level: str = LogLevel.INFO) -> None: :type level: str :return: :obj:`None` """ - raise NotImplementedError() + raise NotImplementedError diff --git a/mops/abstraction/page_abc.py b/mops/abstraction/page_abc.py index 60eea2f7..56405da4 100644 --- a/mops/abstraction/page_abc.py +++ b/mops/abstraction/page_abc.py @@ -1,13 +1,13 @@ from __future__ import annotations from abc import ABC -from typing import Union, TYPE_CHECKING +from typing import TYPE_CHECKING from mops.abstraction.mixin_abc import MixinABC -from mops.base.element import Element from mops.utils.internal_utils import WAIT_PAGE if TYPE_CHECKING: + from mops.base.element import Element from mops.base.page import Page @@ -24,7 +24,7 @@ def reload_page(self, wait_page_load: bool = True) -> Page: :type wait_page_load: bool :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError def open_page(self, url: str = '') -> Page: """ @@ -34,9 +34,9 @@ def open_page(self, url: str = '') -> Page: :type url: str :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError - def wait_page_loaded(self, silent: bool = False, timeout: Union[int, float] = WAIT_PAGE) -> Page: + def wait_page_loaded(self, silent: bool = False, timeout: float = WAIT_PAGE) -> Page: """ Wait until the page is fully loaded by checking the visibility of the anchor element and other page elements. @@ -49,7 +49,7 @@ def wait_page_loaded(self, silent: bool = False, timeout: Union[int, float] = WA :type timeout: Union[int, float] :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError def is_page_opened(self, with_elements: bool = False, with_url: bool = False) -> bool: """ @@ -61,7 +61,7 @@ def is_page_opened(self, with_elements: bool = False, with_url: bool = False) -> :type with_url: bool :return: :obj:`bool` - `True` if the page is opened, otherwise `False`. """ - raise NotImplementedError() + raise NotImplementedError def swipe( self, @@ -70,7 +70,7 @@ def swipe( end_x: int, end_y: int, duration: int = 0, - sleep: Union[int, float] = 0 + sleep: float = 0, ) -> Page: """ Appium only: Swipe from one point to another, with an optional duration and post-swipe delay. @@ -89,7 +89,7 @@ def swipe( :type sleep: Union[int, float] :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError def swipe_down(self) -> Page: """ @@ -97,7 +97,7 @@ def swipe_down(self) -> Page: :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError def swipe_up(self) -> Page: """ @@ -105,4 +105,4 @@ def swipe_up(self) -> Page: :return: :obj:`Page` - The current instance of the page object. """ - raise NotImplementedError() + raise NotImplementedError diff --git a/mops/base/driver_wrapper.py b/mops/base/driver_wrapper.py index 8b8c8d38..a2fb2fc2 100644 --- a/mops/base/driver_wrapper.py +++ b/mops/base/driver_wrapper.py @@ -1,40 +1,41 @@ from __future__ import annotations -from typing import Union, Type, List, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Tuple, Type, Union -from PIL import Image from appium.webdriver.webdriver import WebDriver as AppiumDriver -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver from playwright.sync_api import ( - Page as PlaywrightDriver, Browser as PlaywrightBrowser, BrowserContext as PlaywrightContext, + Page as PlaywrightDriver, ) +from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver -from mops.mixins.objects.box import Box -from mops.mixins.objects.driver import Driver -from mops.visual_comparison import VisualComparison from mops.abstraction.driver_wrapper_abc import DriverWrapperABC +from mops.exceptions import DriverWrapperException +from mops.mixins.internal_mixin import InternalMixin from mops.playwright.play_driver import PlayDriver from mops.selenium.driver.mobile_driver import MobileDriver from mops.selenium.driver.web_driver import WebDriver -from mops.exceptions import DriverWrapperException -from mops.mixins.internal_mixin import InternalMixin from mops.utils.internal_utils import get_attributes_from_object, get_child_elements_with_names from mops.utils.logs import Logging, LogLevel - +from mops.visual_comparison import VisualComparison if TYPE_CHECKING: + from PIL import Image + from mops.base.element import Element + from mops.mixins.objects.box import Box + from mops.mixins.objects.driver import Driver class DriverWrapperSessions: - all_sessions: List[DriverWrapper] = [] + + all_sessions: ClassVar[List[DriverWrapper]] = [] @classmethod def add_session(cls, driver_wrapper: DriverWrapper) -> None: """ - Adds a :obj:`.DriverWrapper` object to the session pool. + Add a :obj:`.DriverWrapper` object to the session pool. :param driver_wrapper: The :obj:`.DriverWrapper` instance to add to the pool. :return: None @@ -44,7 +45,7 @@ def add_session(cls, driver_wrapper: DriverWrapper) -> None: @classmethod def remove_session(cls, driver_wrapper: DriverWrapper) -> None: """ - Removes a :obj:`.DriverWrapper` object from the session pool. + Remove a :obj:`.DriverWrapper` object from the session pool. :param driver_wrapper: The :obj:`.DriverWrapper` instance to remove from the pool. :return: None @@ -123,16 +124,22 @@ class DriverWrapper(InternalMixin, Logging, DriverWrapperABC): browser_name: Union[str, None] = None - def __new__(cls, *args, **kwargs): + def __new__(cls, *_args: Any, **_kwargs: Any): # noqa: D102 if cls.session.sessions_count() == 0: - cls = super().__new__(cls) + mew_cls = super().__new__(cls) else: - cls = super().__new__(type(f'ShadowDriverWrapper', (cls, ), get_attributes_from_object(cls))) # noqa + mew_cls = super().__new__( + type( # noqa: + 'ShadowDriverWrapper', + (cls, ), + get_attributes_from_object(cls), + ), + ) - for name, _ in get_child_elements_with_names(cls, bool).items(): - setattr(cls, name, False) + for name in get_child_elements_with_names(cls, bool): + setattr(mew_cls, name, False) - return cls + return mew_cls def __repr__(self): cls = self.__class__ @@ -147,7 +154,7 @@ def __repr__(self): def __init__(self, driver: Driver): """ - Initializes the DriverWrapper instance based on the provided driver source. + Initialize the DriverWrapper instance based on the provided driver source. This constructor sets up the driver wrapper, which can support Appium, Selenium, or Playwright drivers. @@ -165,7 +172,7 @@ def __init__(self, driver: Driver): self.is_desktop = False self.is_mobile = True - def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): + def quit(self, silent: bool = False, trace_path: str = 'trace.zip') -> None: """ Quit the driver instance. @@ -194,10 +201,10 @@ def save_screenshot( self, file_name: str, screenshot_base: Union[Image, bytes] = None, - convert_type: str = None + convert_type: Optional[str] = None, ) -> Image: """ - Takes a full screenshot of the driver and saves it to the specified path/filename. + Take a full screenshot of the driver and saves it to the specified path/filename. :param file_name: Path or filename for the screenshot. :type file_name: str @@ -207,7 +214,7 @@ def save_screenshot( :type convert_type: str :return: :class:`PIL.Image.Image` """ - self.log(f'Save driver screenshot') + self.log('Save driver screenshot') image_object = screenshot_base if isinstance(screenshot_base, bytes) or screenshot_base is None: @@ -225,14 +232,14 @@ def assert_screenshot( filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, remove: Union[Element, List[Element]] = None, cut_box: Box = None, hide: Union[Element, List[Element]] = None, ) -> None: """ - Asserts that the given screenshot matches the currently taken screenshot. + Assert that the given screenshot matches the currently taken screenshot. :param filename: The full name of the screenshot file. If empty - filename will be generated based on test name & :class:`Element` ``name`` argument & platform. @@ -271,7 +278,7 @@ def assert_screenshot( VisualComparison(self).assert_screenshot( filename=filename, test_name=test_name, name_suffix=name_suffix, threshold=threshold, delay=delay, - scroll=False, remove=remove, fill_background=False, cut_box=cut_box + scroll=False, remove=remove, fill_background=False, cut_box=cut_box, ) def soft_assert_screenshot( @@ -279,14 +286,14 @@ def soft_assert_screenshot( filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, remove: Union[Element, List[Element]] = None, cut_box: Box = None, hide: Union[Element, List[Element]] = None, ) -> Tuple[bool, str]: """ - Compares the currently taken screenshot to the expected screenshot and returns a result. + Compare the currently taken screenshot to the expected screenshot and returns a result. :param filename: The full name of the screenshot file. If empty - filename will be generated based on test name & :class:`Element` ``name`` argument & platform. @@ -319,11 +326,11 @@ def soft_assert_screenshot( self.log(exc, level=LogLevel.ERROR) return False, exc - return True, f'No visual mismatch found for entire screen' + return True, 'No visual mismatch found for entire screen' def __init_base_class__(self) -> None: """ - Get driver wrapper class in according to given driver source, and set him as base class + Get driver wrapper class in according to given driver source, and set him as base class. :return: None """ @@ -339,7 +346,8 @@ def __init_base_class__(self) -> None: self.is_selenium = True self._base_cls = WebDriver else: - raise DriverWrapperException(f'Cant specify {self.__class__.__name__}') + msg = f'Cant specify {self.__class__.__name__}' + raise DriverWrapperException(msg) self._set_static(self._base_cls) self._base_cls.__init__(self, driver_container=self.__driver_container) diff --git a/mops/base/element.py b/mops/base/element.py index 0722b7de..8cfdbfdc 100644 --- a/mops/base/element.py +++ b/mops/base/element.py @@ -1,45 +1,57 @@ from __future__ import annotations from copy import copy -from typing import Union, List, Type, Tuple, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type, Union -from PIL.Image import Image - -from mops.mixins.objects.wait_result import Result -from playwright.sync_api import Page as PlaywrightDriver from appium.webdriver.webdriver import WebDriver as AppiumDriver +from playwright.sync_api import Page as PlaywrightDriver from selenium.common import WebDriverException from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver from mops.abstraction.element_abc import ElementABC -from mops.base.driver_wrapper import DriverWrapper -from mops.exceptions import * +from mops.exceptions import ( + ContinuousWaitException, + DriverWrapperException, + NotInitializedException, + TimeoutException, + UnexpectedElementsCountException, + UnexpectedElementSizeException, + UnexpectedTextException, + UnexpectedValueException, +) +from mops.mixins.driver_mixin import DriverMixin, get_driver_wrapper_from_object +from mops.mixins.internal_mixin import InternalMixin, get_element_info +from mops.mixins.objects.wait_result import Result from mops.playwright.play_element import PlayElement from mops.selenium.elements.mobile_element import MobileElement from mops.selenium.elements.web_element import WebElement -from mops.mixins.driver_mixin import get_driver_wrapper_from_object, DriverMixin -from mops.mixins.internal_mixin import InternalMixin, get_element_info -from mops.mixins.objects.box import Box -from mops.mixins.objects.locator import Locator -from mops.mixins.objects.size import Size -from mops.utils.logs import Logging, LogLevel -from mops.utils.previous_object_driver import PreviousObjectDriver, set_instance_frame -from mops.visual_comparison import VisualComparison -from mops.keyboard_keys import KeyboardKeys +from mops.utils.decorators import wait_condition, wait_continuous from mops.utils.internal_utils import ( + QUARTER_WAIT_EL, WAIT_EL, - is_target_on_screen, - initialize_objects, get_child_elements_with_names, + initialize_objects, + is_page, + is_target_on_screen, safe_getattribute, set_parent_for_attr, - is_page, - QUARTER_WAIT_EL, - wait_condition, ) +from mops.utils.logs import Logging, LogLevel +from mops.utils.previous_object_driver import PreviousObjectDriver, set_instance_frame +from mops.visual_comparison import VisualComparison if TYPE_CHECKING: + from PIL.Image import Image + + from mops.base.driver_wrapper import DriverWrapper from mops.base.group import Group + from mops.keyboard_keys import KeyboardKeys + from mops.mixins.objects.box import Box + from mops.mixins.objects.locator import Locator + from mops.mixins.objects.size import Size + + +# ruff: noqa: ARG002, D102 class Element(DriverMixin, InternalMixin, Logging, ElementABC): @@ -56,24 +68,25 @@ class Element(DriverMixin, InternalMixin, Logging, ElementABC): _base_cls: Type[PlayElement, MobileElement, WebElement] driver_wrapper: DriverWrapper - def __new__(cls, *args, **kwargs): - instance = super(Element, cls).__new__(cls) + def __new__(cls, *_args: Any, **_kwargs: Any): + instance = super().__new__(cls) set_instance_frame(instance) return instance - def __repr__(self): + def __repr__(self) -> str: return self._repr_builder() def __call__(self, driver_wrapper: DriverWrapper = None): self.__full_init__(driver_wrapper=get_driver_wrapper_from_object(driver_wrapper)) return self - def __getattribute__(self, item): + def __getattribute__(self, item: str): if 'element' in item and not safe_getattribute(self, '_initialized'): - raise NotInitializedException( - f'{repr(self)} object is not initialized. ' + msg = ( + f'{self!r} object is not initialized. ' 'Try to initialize base object first or call it directly as a method' ) + raise NotInitializedException(msg) return safe_getattribute(self, item) @@ -84,9 +97,9 @@ def __init__( parent: Union[Group, Element, bool] = None, wait: Optional[bool] = None, driver_wrapper: Union[DriverWrapper, Any] = None, - ): + ) -> None: """ - Initializes an Element based on the current driver. + Initialize an Element based on the current driver. If no driver is available, initialization is skipped and will be handled later in a Page or Group. @@ -105,11 +118,10 @@ def __init__( """ self._validate_inheritance() - if parent: - if not isinstance(parent, (bool, Element)): - error = (f'The given "parent" arg of "{self.name}" should take an Element/Group ' - f'object or False for skip. Get {parent}') - raise ValueError(error) + if parent and not isinstance(parent, (bool, Element)): + error = (f'The given "parent" arg of "{self.name}" should take an Element/Group ' + f'object or False for skip. Get {parent}') + raise ValueError(error) self.locator = locator self.name = name if name else locator @@ -124,7 +136,7 @@ def __init__( if self.driver_wrapper: self.__full_init__(driver_wrapper) - def __full_init__(self, driver_wrapper: Any = None): + def __full_init__(self, driver_wrapper: Union[Any, DriverWrapper] = None): self._driver_wrapper_given = bool(driver_wrapper) if self._driver_wrapper_given and driver_wrapper != self.driver_wrapper: @@ -138,7 +150,7 @@ def __full_init__(self, driver_wrapper: Any = None): def __init_base_class__(self) -> None: """ - Initialise base class according to current driver, and set his methods + Initialise base class according to current driver, and set his methods. :return: None """ @@ -149,7 +161,8 @@ def __init_base_class__(self) -> None: elif isinstance(self.driver, SeleniumDriver): self._base_cls = WebElement else: - raise DriverWrapperException(f'Cant specify {self.__class__.__name__}') + msg = f'Cant specify {self.__class__.__name__}' + raise DriverWrapperException(msg) self._set_static(self._base_cls) self._base_cls.__init__(self) @@ -193,11 +206,61 @@ def send_keyboard_action(self, action: Union[str, KeyboardKeys]) -> Element: # Elements waits - def wait_visibility_without_error(self, *, timeout: Union[int, float] = QUARTER_WAIT_EL, silent: bool = False) -> Element: + @wait_continuous + @wait_condition + def wait_visibility( + self, + *, + timeout: float = WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, + ) -> Element: + """ + Wait until the element becomes visible. + **Note:** The method requires the use of named arguments. + + A continuous visibility verification may be applied for given + or default amount of time after the first condition is met. + + **Selenium:** + + - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration + during the waiting process. + + **Appium:** + + - Applied :func:`wait_condition` decorator integrates an exponential delay + (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases + with each iteration during the waiting process. + + :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. + :type timeout: int + :param silent: If :obj:`True`, suppresses logging. + :type silent: bool + :param continuous: If :obj:`True`, a continuous visibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] + :return: :class:`Element` + """ + return Result( # noqa: + execution_result=self.is_displayed(silent=True), + log=f'Wait until "{self.name}" becomes visible', + exc=TimeoutException(f'"{self.name}" not visible', info=self), + ) + + def wait_visibility_without_error( + self, + *, + timeout: float = QUARTER_WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, + ) -> Element: """ Wait for the element to become visible, without raising an error if it does not. + **Note:** The method requires the use of named arguments. - **Note:** The method requires the use of named arguments. + A continuous visibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium & Playwright:** @@ -214,28 +277,77 @@ def wait_visibility_without_error(self, *, timeout: Union[int, float] = QUARTER_ :type timeout: typing.Union[int, float] :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous visibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ if not silent: - self.log(f'Wait until "{self.name}" becomes visible without error exception') + strategy = 'continuous visible' if continuous else 'hidden' + self.log(f'Wait until "{self.name}" becomes {strategy} without error exception') try: - self.wait_visibility(timeout=timeout, silent=True) - except (TimeoutException, WebDriverException) as exception: + self.wait_visibility(timeout=timeout, silent=True, continuous=continuous) + except (TimeoutException, WebDriverException, ContinuousWaitException) as exception: if not silent: self.log(f'Ignored exception: "{exception.msg}"') return self + @wait_continuous + @wait_condition + def wait_hidden( + self, + *, + timeout: float = WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, + ) -> Element: + """ + Wait until the element becomes hidden. + **Note:** The method requires the use of named arguments. + + A continuous invisibility verification may be applied for given + or default amount of time after the first condition is met. + + **Selenium:** + + - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration + during the waiting process. + + **Appium:** + + - Applied :func:`wait_condition` decorator integrates an exponential delay + (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases + with each iteration during the waiting process. + + :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. + :type timeout: int + :param silent: If :obj:`True`, suppresses logging. + :type silent: bool + :param continuous: If :obj:`True`, a continuous invisibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] + :return: :class:`Element` + """ + return Result( # noqa: + execution_result=self.is_hidden(silent=True), + log=f'Wait until "{self.name}" becomes hidden', + exc=TimeoutException(f'"{self.name}" still visible', info=self), + ) + def wait_hidden_without_error( self, *, - timeout: Union[int, float] = QUARTER_WAIT_EL, - silent: bool = False + timeout: float = QUARTER_WAIT_EL, + silent: bool = False, + continuous: Union[bool, float] = False, ) -> Element: """ Wait for the element to become hidden, without raising an error if it does not. + **Note:** The method requires the use of named arguments. - **Note:** The method requires the use of named arguments. + A continuous invisibility verification may be applied for given + or default amount of time after the first condition is met. **Selenium & Playwright:** @@ -252,25 +364,58 @@ def wait_hidden_without_error( :type timeout: typing.Union[int, float] :param silent: If :obj:`True`, suppresses logging. :type silent: bool + :param continuous: If :obj:`True`, a continuous invisibility verification applied for another 2.5 seconds. + An :obj:`int` or :obj:`float` modifies the continuous wait timeout. + :type continuous: typing.Union[int, float, bool] :return: :class:`Element` """ if not silent: - self.log(f'Wait until "{self.name}" becomes hidden without error exception') + strategy = 'continuous hidden' if continuous else 'hidden' + self.log(f'Wait until "{self.name}" becomes {strategy} without error exception') try: - self.wait_hidden(timeout=timeout, silent=True) - except (TimeoutException, WebDriverException) as exception: + self.wait_hidden(timeout=timeout, silent=silent, continuous=continuous) + except (TimeoutException, WebDriverException, ContinuousWaitException) as exception: if not silent: self.log(f'Ignored exception: "{exception.msg}"') return self + @wait_condition + def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Element: + """ + Wait until the element becomes available in DOM tree. + **Note:** The method requires the use of named arguments. + + **Selenium:** + + - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration + during the waiting process. + + **Appium:** + + - Applied :func:`wait_condition` decorator integrates an exponential delay + (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases + with each iteration during the waiting process. + + :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. + :type timeout: int + :param silent: If :obj:`True`, suppresses logging. + :type silent: bool + :return: :class:`Element` + """ + return Result( # noqa: + execution_result=self.is_available(), + log=f'Wait until presence of "{self.name}"', + exc=TimeoutException(f'"{self.name}" not available in DOM', info=self), + ) + @wait_condition def wait_for_text( self, expected_text: Optional[str] = None, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait for the presence of a specific text in the current element, or for any non-empty text. @@ -307,15 +452,19 @@ def wait_for_text( error = f'Text of "{self.name}" is empty' log_msg = f'Wait for any text of "{self.name}"' - return Result(result, log_msg, UnexpectedTextException(error, actual_text, expected_text)) # noqa + return Result( # noqa: + result, + log_msg, + UnexpectedTextException(error, actual_text, expected_text), + ) @wait_condition def wait_for_value( self, expected_value: Optional[str] = None, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait for a specific value in the current element, or for any non-empty value. @@ -352,10 +501,14 @@ def wait_for_value( error = f'Value of "{self.name}" is empty' log_msg = f'Wait for any value inside "{self.name}"' - return Result(result, log_msg, UnexpectedValueException(error, actual_value, expected_value)) # noqa + return Result( # noqa: + result, + log_msg, + UnexpectedValueException(error, actual_value, expected_value), + ) @wait_condition - def wait_enabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = False) -> Element: + def wait_enabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element: """ Wait for the element to become enabled and/or clickable. @@ -378,14 +531,14 @@ def wait_enabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = F :type silent: bool :return: :class:`Element` """ - return Result( # noqa + return Result( # noqa: execution_result=self.is_enabled(silent=True), log=f'Wait until "{self.name}" becomes enabled', exc=TimeoutException(f'"{self.name}" is not enabled', info=self), ) @wait_condition - def wait_disabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = False) -> Element: + def wait_disabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element: """ Wait for the element to become disabled. @@ -408,7 +561,7 @@ def wait_disabled(self, *, timeout: Union[int, float] = WAIT_EL, silent: bool = :type silent: bool :return: :class:`Element` """ - return Result( # noqa + return Result( # noqa: execution_result=not self.is_enabled(silent=True), log=f'Wait until "{self.name}" becomes disabled', exc=TimeoutException(f'"{self.name}" is not disabled', info=self), @@ -419,11 +572,11 @@ def wait_for_size( self, expected_size: Size, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ - Wait until element size will be equal to given :class:`.Size` object + Wait until element size will be equal to given :class:`.Size` object. **Note:** The method requires the use of named arguments except ``expected_size``. @@ -449,7 +602,7 @@ def wait_for_size( actual = self.size is_height_equal = actual.height == expected_size.height if expected_size.height is not None else True is_width_equal = actual.width == expected_size.width if expected_size.width is not None else True - return Result( # noqa + return Result( # noqa: execution_result=is_height_equal and is_width_equal, log=f'Wait until "{self.name}" size will be equal to {expected_size}', exc=UnexpectedElementSizeException(f'Unexpected size for "{self.name}"', actual, expected_size), @@ -460,8 +613,8 @@ def wait_elements_count( self, expected_count: int, *, - timeout: Union[int, float] = WAIT_EL, - silent: bool = False + timeout: float = WAIT_EL, + silent: bool = False, ) -> Element: """ Wait until the number of matching elements equals the expected count. @@ -489,7 +642,7 @@ def wait_elements_count( """ actual_count = self.get_elements_count(silent=True) error_msg = f'Unexpected elements count of "{self.name}"' - return Result( # noqa + return Result( # noqa: execution_result=actual_count == expected_count, log=f'Wait until elements count of "{self.name}" will be equal to "{expected_count}"', exc=UnexpectedElementsCountException(error_msg, actual_count, expected_count), @@ -504,13 +657,14 @@ def all_elements(self) -> Union[List[Element], List[Any]]: :return: A list of wrapped :class:`Element` objects. """ if getattr(self, '_wrapped', None): - raise RecursionError(f'all_elements property already used for {self.name}') + msg = f'all_elements property already used for {self.name}' + raise RecursionError(msg) return self._base_cls.all_elements.fget(self) def is_visible(self, check_displaying: bool = True, silent: bool = False) -> bool: """ - Checks is the current element's top-left corner or bottom-right corner is visible on the screen. + Check is the current element's top-left corner or bottom-right corner is visible on the screen. :param check_displaying: If :obj:`True`, the :func:`is_displayed` method will be called to further verify visibility. The check will stop if this method returns :obj:`False`. @@ -538,7 +692,7 @@ def is_visible(self, check_displaying: bool = True, silent: bool = False) -> boo def is_fully_visible(self, check_displaying: bool = True, silent: bool = False) -> bool: """ - Check is current element top left corner and bottom right corner visible on current screen + Check is current element top left corner and bottom right corner visible on current screen. :param check_displaying: If :obj:`True`, the :func:`is_displayed` method will be called to further verify visibility. The check will stop if this method returns :obj:`False`. @@ -568,10 +722,10 @@ def save_screenshot( self, file_name: str, screenshot_base: Union[bytes, Image] = None, - convert_type: str = None + convert_type: Optional[str] = None, ) -> Image: """ - Saves a screenshot of the element. + Save a screenshot of the element. :param file_name: Path or filename for the screenshot. :type file_name: str @@ -603,9 +757,9 @@ def hide(self) -> Element: self.execute_script('arguments[0].style.opacity = "0";') return self - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ - Executes a JavaScript script on the element. + Execute a JavaScript script on the element. :param script: JavaScript code to be executed, referring to the element as ``arguments[0]``. :type script: str @@ -613,15 +767,15 @@ def execute_script(self, script: str, *args) -> Any: that appear in script as ``arguments[1]`` ``arguments[2]`` etc. :return: :obj:`typing.Any` result from the script. """ - return self.driver_wrapper.execute_script(script, *[self, *[arg for arg in args]]) + return self.driver_wrapper.execute_script(script, self, *args) def assert_screenshot( self, filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, scroll: bool = False, remove: Union[Element, List[Element]] = None, fill_background: Union[str, bool] = False, @@ -629,7 +783,7 @@ def assert_screenshot( hide: Union[Element, List[Element]] = None, ) -> None: """ - Asserts that the given screenshot matches the currently taken screenshot. + Assert that the given screenshot matches the currently taken screenshot. :param filename: The full name of the screenshot file. If empty - filename will be generated based on test name & :class:`Element` ``name`` argument & platform. @@ -673,7 +827,7 @@ def assert_screenshot( VisualComparison(self.driver_wrapper, self).assert_screenshot( filename=filename, test_name=test_name, name_suffix=name_suffix, threshold=threshold, delay=delay, - scroll=scroll, remove=remove, fill_background=fill_background, cut_box=cut_box + scroll=scroll, remove=remove, fill_background=fill_background, cut_box=cut_box, ) def soft_assert_screenshot( @@ -681,8 +835,8 @@ def soft_assert_screenshot( filename: str = '', test_name: str = '', name_suffix: str = '', - threshold: Union[int, float] = None, - delay: Union[int, float] = None, + threshold: Optional[float] = None, + delay: Optional[float] = None, scroll: bool = False, remove: Union[Element, List[Element]] = None, fill_background: Union[str, bool] = False, @@ -690,7 +844,7 @@ def soft_assert_screenshot( hide: Union[Element, List[Element]] = None, ) -> Tuple[bool, str]: """ - Compares the currently taken screenshot to the expected screenshot and returns a result. + Compare the currently taken screenshot to the expected screenshot and returns a result. :param filename: The full name of the screenshot file. If empty - filename will be generated based on test name & :class:`Element` ``name`` argument & platform. @@ -723,7 +877,7 @@ def soft_assert_screenshot( """ try: self.assert_screenshot( - filename, test_name, name_suffix, threshold, delay, scroll, remove, fill_background, cut_box, hide + filename, test_name, name_suffix, threshold, delay, scroll, remove, fill_background, cut_box, hide, ) except AssertionError as exc: exc = str(exc) @@ -734,7 +888,7 @@ def soft_assert_screenshot( def get_element_info(self, element: Optional[Element] = None) -> str: """ - Retrieves detailed logging information for the specified element. + Retrieve detailed logging information for the specified element. :param element: The :class:`Element` for which to collect logging data. If :obj:`None`, logging data for the ``parent`` element is used. @@ -746,7 +900,7 @@ def get_element_info(self, element: Optional[Element] = None) -> str: def _get_all_elements(self, sources: Union[tuple, list]) -> List[Any]: """ - Retrieves all wrapped elements from the given sources. + Retrieve all wrapped elements from the given sources. :param sources: A list or tuple of source objects :type sources: tuple or list @@ -757,32 +911,33 @@ def _get_all_elements(self, sources: Union[tuple, list]) -> List[Any]: for element in sources: wrapped_object: Any = copy(self) wrapped_object.element = element - wrapped_object._wrapped = True + wrapped_object._wrapped = True # noqa: SLF001 set_parent_for_attr(wrapped_object, Element, with_copy=True) wrapped_elements.append(wrapped_object) return wrapped_elements - def _modify_children(self): + def _modify_children(self) -> None: """ - Initializing of attributes with type == Element. + Initialize of attributes with type == Element. Required for classes with base == Element. """ initialize_objects(self, get_child_elements_with_names(self, Element), Element) - def _modify_object(self): + def _modify_object(self) -> None: """ Modify current object if driver_wrapper is not given. Required for Page that placed into functions: - - sets driver from previous object + - sets driver from previous object. """ if not self._driver_wrapper_given: PreviousObjectDriver().set_driver_from_previous_object(self) - def _validate_inheritance(self): + def _validate_inheritance(self) -> None: cls = self.__class__ mro = cls.__mro__ for item in mro: if is_page(item): + msg = f'You cannot make an inheritance for {cls.__name__} from both Element/Group and Page objects' raise TypeError( - f"You cannot make an inheritance for {cls.__name__} from both Element/Group and Page objects") + msg) diff --git a/mops/base/group.py b/mops/base/group.py index ba711190..a5141183 100644 --- a/mops/base/group.py +++ b/mops/base/group.py @@ -1,17 +1,19 @@ from __future__ import annotations -from typing import Any, Union, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, Union -from mops.base.driver_wrapper import DriverWrapper from mops.base.element import Element -from mops.mixins.objects.locator import Locator from mops.utils.internal_utils import ( - set_parent_for_attr, get_child_elements, + get_child_elements_with_names, initialize_objects, - get_child_elements_with_names + set_parent_for_attr, ) +if TYPE_CHECKING: + from mops.base.driver_wrapper import DriverWrapper + from mops.mixins.objects.locator import Locator + class Group(Element): """ @@ -42,7 +44,7 @@ def __init__( driver_wrapper: Union[DriverWrapper, Any] = None, ): """ - Initializes a group of elements based on the current driver. + Initialize a group of elements based on the current driver. If no driver is provided, the initialization will be skipped until handled by a :class:`.Page` or :class:`.Group` class. @@ -74,7 +76,7 @@ def __init__( def _modify_children(self) -> None: """ - Initializing of attributes with type == Group/Element. + Initialize of attributes with type == Group/Element. Required for classes with base == Group. """ initialize_objects(self, get_child_elements_with_names(self, Element), Element) diff --git a/mops/base/page.py b/mops/base/page.py index 3f871150..cd126ddd 100644 --- a/mops/base/page.py +++ b/mops/base/page.py @@ -1,30 +1,32 @@ from __future__ import annotations -from typing import Union, Any, List, Type +from typing import TYPE_CHECKING, Any, List, Type, Union -from playwright.sync_api import Page as PlaywrightDriver from appium.webdriver.webdriver import WebDriver as AppiumDriver +from playwright.sync_api import Page as PlaywrightDriver from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver from mops.abstraction.page_abc import PageABC -from mops.base.driver_wrapper import DriverWrapper from mops.base.element import Element +from mops.exceptions import DriverWrapperException +from mops.mixins.driver_mixin import DriverMixin, get_driver_wrapper_from_object +from mops.mixins.internal_mixin import InternalMixin from mops.playwright.play_page import PlayPage from mops.selenium.pages.mobile_page import MobilePage from mops.selenium.pages.web_page import WebPage -from mops.exceptions import DriverWrapperException -from mops.mixins.driver_mixin import get_driver_wrapper_from_object, DriverMixin -from mops.mixins.internal_mixin import InternalMixin -from mops.mixins.objects.locator import Locator -from mops.utils.logs import Logging -from mops.utils.previous_object_driver import PreviousObjectDriver, set_instance_frame from mops.utils.internal_utils import ( WAIT_PAGE, - initialize_objects, - get_child_elements_with_names, get_child_elements, + get_child_elements_with_names, + initialize_objects, is_element_instance, ) +from mops.utils.logs import Logging +from mops.utils.previous_object_driver import PreviousObjectDriver, set_instance_frame + +if TYPE_CHECKING: + from mops.base.driver_wrapper import DriverWrapper + from mops.mixins.objects.locator import Locator class Page(DriverMixin, InternalMixin, Logging, PageABC): @@ -46,7 +48,7 @@ class Page(DriverMixin, InternalMixin, Logging, PageABC): anchor: Element def __new__(cls, *args, **kwargs): - instance = super(Page, cls).__new__(cls) + instance = super().__new__(cls) set_instance_frame(instance) return instance @@ -57,7 +59,7 @@ def __init__( self, locator: Union[Locator, str] = '', name: str = '', - driver_wrapper: Union[DriverWrapper, Any] = None + driver_wrapper: Union[DriverWrapper, Any] = None, ): """ Initializes a Page based on the current driver. @@ -73,7 +75,7 @@ def __init__( self._validate_inheritance() self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper) - + self.anchor = Element(locator, name=name, driver_wrapper=self.driver_wrapper) self.locator = self.anchor.locator self.locator_type = self.anchor.locator_type @@ -93,7 +95,7 @@ def __init__( def __init_base_class__(self) -> None: """ - Initialise base class according to current driver, and set his methods + Initialise base class according to current driver, and set his methods. :return: None """ @@ -104,7 +106,8 @@ def __init_base_class__(self) -> None: elif isinstance(self.driver, SeleniumDriver): self._base_cls = WebPage else: - raise DriverWrapperException(f'Cant specify {Page.__name__}') + msg = f'Cant specify {Page.__name__}' + raise DriverWrapperException(msg) self._set_static(self._base_cls) self._base_cls.__init__(self) @@ -136,12 +139,12 @@ def open_page(self, url: str = '') -> Page: :type url: str :return: :obj:`Page` - The current instance of the page object. """ - url = self.url if not url else url + url = url if url else self.url self.driver_wrapper.get(url) self.wait_page_loaded() return self - def wait_page_loaded(self, silent: bool = False, timeout: Union[int, float] = WAIT_PAGE) -> Page: + def wait_page_loaded(self, silent: bool = False, timeout: float = WAIT_PAGE) -> Page: """ Wait until the page is fully loaded by checking the visibility of the anchor element and other page elements. @@ -160,9 +163,9 @@ def wait_page_loaded(self, silent: bool = False, timeout: Union[int, float] = WA self.anchor.wait_visibility(timeout=timeout, silent=True) for element in self.page_elements: - if getattr(element, 'wait') is False: + if element.wait is False: element.wait_hidden(timeout=timeout, silent=True) - elif getattr(element, 'wait') is True: + elif element.wait is True: element.wait_visibility(timeout=timeout, silent=True) return self @@ -180,7 +183,7 @@ def is_page_opened(self, with_elements: bool = False, with_url: bool = False) -> if with_elements: for element in self.page_elements: - if getattr(element, 'wait'): + if element.wait: result &= element.is_displayed(silent=True) if not result: self.log(f'Element "{element.name}" is not displayed', level='debug') @@ -192,26 +195,27 @@ def is_page_opened(self, with_elements: bool = False, with_url: bool = False) -> return result - def _modify_children(self): + def _modify_children(self) -> None: """ Initializing of attributes with type == Element. Required for classes with base == Page. """ initialize_objects(self, get_child_elements_with_names(self, Element), Element) - def _modify_page_driver_wrapper(self, driver_wrapper: Any): + def _modify_page_driver_wrapper(self, driver_wrapper: Any) -> None: """ Modify current object if driver_wrapper is not given. Required for Page that placed into functions: - - sets driver from previous object + - sets driver from previous object. """ if not driver_wrapper: PreviousObjectDriver().set_driver_from_previous_object(self) - def _validate_inheritance(self): + def _validate_inheritance(self) -> None: cls = self.__class__ mro = cls.__mro__ for item in mro: if is_element_instance(item): + msg = f'You cannot make an inheritance for {cls.__name__} from both Page and Group/Element objects' raise TypeError( - f"You cannot make an inheritance for {cls.__name__} from both Page and Group/Element objects") + msg) diff --git a/mops/exceptions.py b/mops/exceptions.py index 52dbf3c9..42ed9736 100644 --- a/mops/exceptions.py +++ b/mops/exceptions.py @@ -2,9 +2,7 @@ class DriverWrapperException(Exception): - """ - Base driver wrapper exceptions - """ + """Base driver wrapper exceptions.""" def __init__( self, @@ -12,7 +10,7 @@ def __init__( actual: Any = None, expected: Any = None, timeout: Any = None, - info: Any = None + info: Any = None, ): self._msg = '' self._original_msg = msg @@ -23,7 +21,7 @@ def __init__( self.__suppress_context__ = True def __str__(self) -> str: - return f"\nMessage: {self.msg}" + return f'\nMessage: {self.msg}' @property def msg(self): @@ -41,7 +39,7 @@ def msg(self): def wrap_by_quotes(self, data): if data is None: - data = "" + data = '' if isinstance(data, str): return f'"{data}"' @@ -50,84 +48,56 @@ def wrap_by_quotes(self, data): class UnexpectedElementsCountException(DriverWrapperException): - """ - Thrown when elements count isn't equal to expected - """ - pass + """Thrown when elements count isn't equal to expected.""" class UnexpectedElementSizeException(DriverWrapperException): - """ - Thrown when element size isn't equal to expected - """ - pass + """Thrown when element size isn't equal to expected.""" class UnexpectedValueException(DriverWrapperException): - """ - Thrown when element contains incorrect value - """ - pass + """Thrown when element contains incorrect value.""" class UnexpectedTextException(DriverWrapperException): - """ - Thrown when element contains incorrect text - """ - pass + """Thrown when element contains incorrect text.""" class TimeoutException(DriverWrapperException): - """ - Thrown when timeout exceeded - """ - pass + """Thrown when timeout exceeded.""" class InvalidSelectorException(DriverWrapperException): - """ - Thrown when element have invalid selector - """ - pass + """Thrown when element have invalid selector.""" class NoSuchElementException(DriverWrapperException): - """ - Thrown when element could not be found - """ - pass + """Thrown when element could not be found.""" class NoSuchParentException(DriverWrapperException): - """ - Thrown when parent could not be found - """ - pass + """Thrown when parent could not be found.""" class ElementNotInteractableException(DriverWrapperException): - """ - Thrown when element found and enabled but not interactable - """ - pass + """Thrown when element found and enabled but not interactable.""" class UnsuitableArgumentsException(DriverWrapperException): - """ - Thrown when object initialised with unsuitable arguments - """ - pass + """Thrown when object initialised with unsuitable arguments.""" class NotInitializedException(DriverWrapperException): - """ - Thrown when getting access to not initialized object - """ - pass + """Thrown when getting access to not initialized object.""" class InvalidLocatorException(DriverWrapperException): - """ - Thrown when locator is invalid - """ - pass + """Thrown when locator is invalid.""" + + +class ContinuousWaitException(DriverWrapperException): + """Thrown when continuous wait is failed.""" + + +class VisualComparisonException(DriverWrapperException): + """Thrown when visual comparison error occur.""" diff --git a/mops/js_scripts.py b/mops/js_scripts.py index 0777011b..1f76231c 100644 --- a/mops/js_scripts.py +++ b/mops/js_scripts.py @@ -42,27 +42,37 @@ add_element_over_js = """ function appendElement(given_obj) { - given_obj = given_obj.getBoundingClientRect(); - driver_wrapper_obj = document.createElement("div"); - - driver_wrapper_obj.style.zIndex=9999999; - driver_wrapper_obj.setAttribute("class","driver-wrapper-comparison-support-element"); + const rect = given_obj.getBoundingClientRect(); + const driverWrapperObj = document.createElement("div"); + + driverWrapperObj.style.zIndex = 9999999; + driverWrapperObj.setAttribute("class", "driver-wrapper-comparison-support-element"); + driverWrapperObj.style.backgroundColor = "#000"; + + // Determine position type based on scroll position + if (window.scrollY === 0) { + driverWrapperObj.style.position = "fixed"; + driverWrapperObj.style.top = rect.top + "px"; + driverWrapperObj.style.left = rect.left + "px"; + } else { + driverWrapperObj.style.position = "absolute"; + driverWrapperObj.style.top = (rect.top + window.scrollY) + "px"; + driverWrapperObj.style.left = (rect.left + window.scrollX) + "px"; + } - driver_wrapper_obj.style.position = "absolute"; - driver_wrapper_obj.style.backgroundColor = "#000"; + driverWrapperObj.style.width = rect.width + "px"; + driverWrapperObj.style.height = rect.height + "px"; - driver_wrapper_obj.style.width = given_obj.width + "px"; - driver_wrapper_obj.style.height = given_obj.height + "px"; - driver_wrapper_obj.style.top = (given_obj.top + window.scrollY) + "px"; - driver_wrapper_obj.style.left = (given_obj.left + window.scrollX) + "px"; - - document.body.appendChild(driver_wrapper_obj); -}; + document.body.appendChild(driverWrapperObj); +} return appendElement(arguments[0]); """ +hide_caret_js_script = 'document.activeElement.blur();' + + add_driver_index_comment_js = """ function addComment(driver_index) { comment = document.createComment(" " + driver_index + " "); diff --git a/mops/keyboard_keys.py b/mops/keyboard_keys.py index 05421264..58269293 100644 --- a/mops/keyboard_keys.py +++ b/mops/keyboard_keys.py @@ -81,11 +81,10 @@ class PlaywrightKeys: class Interceptor(type): - def __getattribute__(self, item): + def __getattribute__(cls, item): if DriverWrapper.is_selenium: return getattr(SeleniumKeys, item) - else: - return getattr(PlaywrightKeys, item, NotImplementedError(f'Key is not added to Mops framework')) + return getattr(PlaywrightKeys, item, NotImplementedError('Key is not added to Mops framework')) class KeyboardKeys(SeleniumKeys, PlaywrightKeys, metaclass=Interceptor): diff --git a/mops/mixins/driver_mixin.py b/mops/mixins/driver_mixin.py index 236b1a50..ccaeb83a 100644 --- a/mops/mixins/driver_mixin.py +++ b/mops/mixins/driver_mixin.py @@ -1,13 +1,14 @@ from __future__ import annotations -from typing import Union, Any - -from appium.webdriver.webdriver import WebDriver as AppiumWebDriver -from playwright.sync_api import Page as PlaywrightSourcePage -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver +from typing import TYPE_CHECKING, Any, Union from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver as AppiumWebDriver + from playwright.sync_api import Page as PlaywrightSourcePage + from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver + def get_driver_wrapper_from_object(obj: Union[DriverWrapper, Any]): """ @@ -28,8 +29,9 @@ def get_driver_wrapper_from_object(obj: Union[DriverWrapper, Any]): elif hasattr(obj, 'driver_wrapper'): driver_wrapper_instance = obj.driver_wrapper else: - obj_nfo = f'"{getattr(obj, "name")}" of "{obj.__class__}"' if obj else obj - raise Exception(f'Cant get driver_wrapper from {obj_nfo}') + obj_nfo = f'"{obj.name}" of "{obj.__class__}"' if obj else obj + msg = f'Cant get driver_wrapper from {obj_nfo}' + raise Exception(msg) return driver_wrapper_instance @@ -40,7 +42,7 @@ class DriverMixin: @property def driver(self) -> Union[SeleniumWebDriver, AppiumWebDriver, PlaywrightSourcePage]: - """ + r""" Retrieves the source driver instance, which could be a Selenium, Appium, or Playwright driver. :return: Current source driver that assigned for this object, which is either \n @@ -61,7 +63,7 @@ def driver_wrapper(self) -> DriverWrapper: return self._driver_wrapper @driver_wrapper.setter - def driver_wrapper(self, driver_wrapper: DriverWrapper): + def driver_wrapper(self, driver_wrapper: DriverWrapper) -> None: """ Sets the driver wrapper instance, for this object. diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py index ea250f24..9a1da1f3 100644 --- a/mops/mixins/internal_mixin.py +++ b/mops/mixins/internal_mixin.py @@ -1,24 +1,23 @@ from __future__ import annotations from functools import lru_cache -from typing import Any +from typing import Any, Optional from appium.webdriver.common.appiumby import AppiumBy from mops.utils.internal_utils import ( - get_child_elements_with_names, - get_child_elements, get_all_attributes_from_object, + get_child_elements, + get_child_elements_with_names, ) - all_locator_types = get_child_elements(AppiumBy, str) available_kwarg_keys = ('desktop', 'mobile', 'ios', 'android') def get_element_info(element: Any, label: str = 'Selector=') -> str: """ - Get element selector information with parent object selector if it exists + Get element selector information with parent object selector if it exists. :param element: element to collect log data :param label: a label before selector string @@ -38,25 +37,25 @@ def get_static(cls: Any): class InternalMixin: - def _safe_setter(self, var: str, value: Any): + def _safe_setter(self, var: str, value: Any) -> None: if not hasattr(self, var): setattr(self, var, value) def _set_static(self: Any, cls) -> None: """ - Set static from base cls (Web/Mobile/Play Element/Page etc.) + Set static from base cls (Web/Mobile/Play Element/Page etc.). :return: None """ data = { name: value for name, value in get_static(cls) - if name not in get_all_attributes_from_object(self).keys() + if name not in get_all_attributes_from_object(self) }.items() for name, item in data: setattr(self.__class__, name, item) - def _repr_builder(self: Any): + def _repr_builder(self: Any) -> Optional[str]: class_name = self.__class__.__name__ obj_id = hex(id(self)) parent = getattr(self, 'parent', False) diff --git a/mops/mixins/native_context.py b/mops/mixins/native_context.py index cbf11e38..6ef8bbe7 100644 --- a/mops/mixins/native_context.py +++ b/mops/mixins/native_context.py @@ -31,7 +31,7 @@ def __init__(self, driver_wrapper): self.top_bar = Element( Locator(mobile=self.ios_mobile_top_bar_locator, tablet=self.ios_tablet_top_bar_locator), name='safari top bar', - driver_wrapper=driver_wrapper + driver_wrapper=driver_wrapper, ) self.bottom_bar = Element(self.ios_mobile_small_bottom_bar_locator, name='safari bottom bar', @@ -41,7 +41,7 @@ def __init__(self, driver_wrapper): def get_bottom_bar_height(self) -> int: """ - Get iOS/iPadOS bottom bar height + Get iOS/iPadOS bottom bar height. :return: int """ diff --git a/mops/mixins/objects/box.py b/mops/mixins/objects/box.py index c066a50c..dcf952f4 100644 --- a/mops/mixins/objects/box.py +++ b/mops/mixins/objects/box.py @@ -1,11 +1,13 @@ from __future__ import annotations +from dataclasses import InitVar, dataclass import typing -from dataclasses import dataclass, InitVar -from mops.mixins.objects.size import Size from mops.utils.internal_utils import get_dict +if typing.TYPE_CHECKING: + from mops.mixins.objects.size import Size + @dataclass class Box: @@ -15,6 +17,7 @@ class Box: The class allows specifying these edges as absolute values or percentages of an image's dimensions. It includes methods to fill missing values and calculate the coordinates of the cut box based on an image's size. """ + left: typing.Union[int, float, None] = None top: typing.Union[int, float, None] = None right: typing.Union[int, float, None] = None @@ -50,7 +53,7 @@ def get_image_cut_box(self, size: Size) -> Box: width, height = size.width, size.height self.fill_values() - if self.is_percents: # noqa + if self.is_percents: left = self.left * width / 100 if self.left else self.left top = self.top * height / 100 if self.top else self.top right = width - self.right * width / 100 if self.right else width diff --git a/mops/mixins/objects/driver.py b/mops/mixins/objects/driver.py index 753dafa1..d0661671 100644 --- a/mops/mixins/objects/driver.py +++ b/mops/mixins/objects/driver.py @@ -2,12 +2,12 @@ from typing import Union from appium.webdriver.webdriver import WebDriver as AppiumDriver -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver from playwright.sync_api import ( Browser as PlaywrightBrowser, BrowserContext as PlaywrightContext, Page as PlaywrightDriver, ) +from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver @dataclass diff --git a/mops/mixins/objects/location.py b/mops/mixins/objects/location.py index 251dc2a8..b24b90cc 100644 --- a/mops/mixins/objects/location.py +++ b/mops/mixins/objects/location.py @@ -1,10 +1,10 @@ -import typing from dataclasses import dataclass +import typing @dataclass class Location: - """ Represents a location on a web UI element, defined by its `x` and `y` coordinates. """ + """Represents a location on a web UI element, defined by its `x` and `y` coordinates.""" x: typing.Union[int, float, None] = None y: typing.Union[int, float, None] = None diff --git a/mops/mixins/objects/locator.py b/mops/mixins/objects/locator.py index 03513f20..b46260fd 100644 --- a/mops/mixins/objects/locator.py +++ b/mops/mixins/objects/locator.py @@ -4,11 +4,11 @@ @dataclass class Locator: - """ Represents a WEB UI element locator with platform-specific variations. """ + """Represents a WEB UI element locator with platform-specific variations.""" default: Optional[str] = None """ - All: The default locator for the object, used by default if no other locators are specified + All: The default locator for the object, used by default if no other locators are specified or if no specific platform/device type is detected. """ @@ -25,18 +25,18 @@ class Locator: tablet: Optional[str] = None """ - Appium only: The locator specifically for tablet devices, useful for web and app automation on tablets. + Appium only: The locator specifically for tablet devices, useful for web and app automation on tablets. The "is_tablet: True" desired_capability is required. """ ios: Optional[str] = None """ - Appium only: The locator specifically for iOS devices, + Appium only: The locator specifically for iOS devices, allowing for targeting locators specific to iOS applications. """ android: Optional[str] = None """ - Appium only: The locator specifically for Android devices, + Appium only: The locator specifically for Android devices, allowing for targeting locators specific to Android applications. """ diff --git a/mops/mixins/objects/locator_type.py b/mops/mixins/objects/locator_type.py index 71c4b246..4c568415 100644 --- a/mops/mixins/objects/locator_type.py +++ b/mops/mixins/objects/locator_type.py @@ -22,6 +22,7 @@ class LocatorType: - :obj:`Element(f'{LocatorType.XPATH}=//*[@class="class-name"]')` """ + CSS: str = 'css' XPATH: str = 'xpath' ID: str = 'id' diff --git a/mops/mixins/objects/scrolls.py b/mops/mixins/objects/scrolls.py index 90fcb8f5..37ea40e2 100644 --- a/mops/mixins/objects/scrolls.py +++ b/mops/mixins/objects/scrolls.py @@ -1,5 +1,5 @@ class ScrollTo: - """ Defines available scroll positioning options. """ + """Defines available scroll positioning options.""" START: str = 'start' CENTER: str = 'center' @@ -8,7 +8,7 @@ class ScrollTo: class ScrollTypes: - """ Defines available scroll behaviors. """ + """Defines available scroll behaviors.""" SMOOTH: str = 'smooth' INSTANT: str = 'instant' diff --git a/mops/mixins/objects/size.py b/mops/mixins/objects/size.py index 169fa722..7081b98f 100644 --- a/mops/mixins/objects/size.py +++ b/mops/mixins/objects/size.py @@ -1,10 +1,10 @@ -import typing from dataclasses import dataclass +import typing @dataclass class Size: - """ Represents the dimensions of an object with width and height. """ + """Represents the dimensions of an object with width and height.""" width: typing.Union[int, float, None] = None height: typing.Union[int, float, None] = None diff --git a/mops/playwright/play_driver.py b/mops/playwright/play_driver.py index fee54f6b..b9fc6cd3 100644 --- a/mops/playwright/play_driver.py +++ b/mops/playwright/play_driver.py @@ -1,30 +1,31 @@ from __future__ import annotations +import contextlib from dataclasses import asdict from functools import cached_property -from typing import List, Union, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, List, Optional -from playwright._impl._errors import Error as PlaywrightError # noqa - -from PIL import Image -from playwright.sync_api import Locator, Page, Browser, BrowserContext +from playwright._impl._errors import Error as PlaywrightError +from playwright.sync_api import Browser, BrowserContext, Locator, Page from mops.abstraction.driver_wrapper_abc import DriverWrapperABC -from mops.mixins.objects.driver import Driver from mops.mixins.objects.size import Size from mops.shared_utils import get_image -from mops.utils.internal_utils import get_timeout_in_ms, WAIT_UNIT +from mops.utils.internal_utils import WAIT_UNIT, get_timeout_in_ms from mops.utils.logs import Logging if TYPE_CHECKING: + from PIL import Image + from mops.base.element import Element + from mops.mixins.objects.driver import Driver class PlayDriver(Logging, DriverWrapperABC): def __init__(self, driver_container: Driver): """ - Initializing of desktop web driver with playwright + Initializing of desktop web driver with playwright. :param driver_container: Driver that contains playwright instance, context and driver objects """ @@ -66,7 +67,7 @@ def is_firefox(self) -> bool: """ return self.browser_name.lower() == 'firefox' - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> PlayDriver: + def wait(self, timeout: float = WAIT_UNIT) -> PlayDriver: """ Pauses the execution for a specified amount of time. @@ -149,7 +150,7 @@ def go_back(self) -> PlayDriver: self.driver.go_back() return self - def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): + def quit(self, silent: bool = False, trace_path: str = 'trace.zip') -> None: """ Quit the driver instance. @@ -169,10 +170,8 @@ def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): :return: :obj:`None` """ if trace_path: - try: + with contextlib.suppress(PlaywrightError): self.context.tracing.stop(path=trace_path) - except PlaywrightError: - pass self._base_driver.close() self.context.close() @@ -325,10 +324,10 @@ def get_window_size(self) -> Size: height = self.execute_script('return window.outerHeight') return Size(width=width, height=height) - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web page. - Appium iOS: Removes native controls from image manually + Appium iOS: Removes native controls from image manually. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -386,10 +385,7 @@ def switch_to_tab(self, tab: int = -1) -> PlayDriver: :type tab: int :return: :obj:`.PlayDriver` - The current instance of the driver wrapper, now switched to the specified tab. """ - if tab == -1: - tab = self.get_all_tabs()[tab] - else: - tab = self.get_all_tabs()[tab - 1] + tab = self.get_all_tabs()[tab] if tab == -1 else self.get_all_tabs()[tab - 1] self.driver = tab self.driver.bring_to_front() diff --git a/mops/playwright/play_element.py b/mops/playwright/play_element.py index a9cbb4e6..b5446f5f 100644 --- a/mops/playwright/play_element.py +++ b/mops/playwright/play_element.py @@ -1,30 +1,37 @@ from __future__ import annotations -import time from abc import ABC -from typing import Union, List, Any +import time +from typing import TYPE_CHECKING, Any, List, Optional, Union + +from playwright.sync_api import ( + Browser, + BrowserContext, + Error, + Locator, + Page, + Page as PlaywrightPage, +) -from PIL.Image import Image -from mops.keyboard_keys import KeyboardKeys +from mops.abstraction.element_abc import ElementABC +from mops.exceptions import InvalidSelectorException +from mops.mixins.objects.location import Location from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes -from playwright.sync_api import TimeoutError as PlayTimeoutError, Error -from playwright.sync_api import Page as PlaywrightPage -from playwright.sync_api import Locator, Page, Browser, BrowserContext - from mops.mixins.objects.size import Size -from mops.mixins.objects.location import Location -from mops.utils.selector_synchronizer import get_platform_locator, set_playwright_locator -from mops.abstraction.element_abc import ElementABC -from mops.exceptions import TimeoutException, InvalidSelectorException -from mops.utils.logs import Logging from mops.shared_utils import cut_log_data, get_image +from mops.utils.decorators import retry from mops.utils.internal_utils import ( - WAIT_EL, - get_timeout_in_ms, calculate_coordinate_to_click, - is_group, is_element, + is_group, ) +from mops.utils.logs import Logging +from mops.utils.selector_synchronizer import get_platform_locator, set_playwright_locator + +if TYPE_CHECKING: + from PIL.Image import Image + + from mops.keyboard_keys import KeyboardKeys class PlayElement(ElementABC, Logging, ABC): @@ -35,10 +42,8 @@ class PlayElement(ElementABC, Logging, ABC): parent: Union[ElementABC, PlayElement] _element: Locator = None - def __init__(self): # noqa - """ - Initializing of web element with playwright driver - """ + def __init__(self): + """Initializing of web element with playwright driver.""" self.locator = get_platform_locator(self) set_playwright_locator(self) @@ -47,7 +52,7 @@ def __init__(self): # noqa @property def element(self) -> Locator: """ - Get playwright Locator object + Get playwright Locator object. :param: args: args from Locator object :param: kwargs: kwargs from Locator object @@ -61,14 +66,14 @@ def element(self) -> Locator: return element @element.setter - def element(self, base_element: Union[Locator, None]): + def element(self, base_element: Union[Locator, None]) -> None: """ - Element object setter. Try to avoid usage of this function + Element object setter. Try to avoid usage of this function. :param: play_element: playwright Locator object """ self._element = base_element - + @property def all_elements(self) -> Union[List[PlayElement], List[Any]]: """ @@ -241,107 +246,13 @@ def uncheck(self) -> PlayElement: return self - # Element waits - - def wait_visibility(self, *, timeout: int = WAIT_EL, silent: bool = False) -> PlayElement: - """ - Waits until the element becomes visible. - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`PlayElement` - """ - if not silent: - self.log(f'Wait until "{self.name}" becomes visible') - - try: - self._first_element.wait_for(state='visible', timeout=get_timeout_in_ms(timeout)) - except PlayTimeoutError: - raise TimeoutException(f'"{self.name}" not visible', timeout=timeout, info=self) - return self - - def wait_hidden(self, *, timeout: int = WAIT_EL, silent: bool = False) -> PlayElement: - """ - Waits until the element becomes hidden. - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`PlayElement` - """ - if not silent: - self.log(f'Wait until "{self.name}" becomes hidden') - try: - self._first_element.wait_for(state='hidden', timeout=get_timeout_in_ms(timeout)) - except PlayTimeoutError: - raise TimeoutException(f'"{self.name}" still visible', timeout=timeout, info=self) - return self - - def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> PlayElement: - """ - Waits until the element becomes available in DOM tree. \n - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`PlayElement` - """ - if not silent: - self.log(f'Wait until presence of "{self.name}"') - - try: - self._first_element.wait_for(state='attached', timeout=get_timeout_in_ms(timeout)) - except PlayTimeoutError: - raise TimeoutException(f'"{self.name}" not available in DOM', timeout=timeout, info=self) - return self - # Element state def scroll_into_view( self, block: ScrollTo = ScrollTo.CENTER, behavior: ScrollTypes = ScrollTypes.INSTANT, - sleep: Union[int, float] = 0, + sleep: float = 0, silent: bool = False, ) -> PlayElement: """ @@ -367,10 +278,10 @@ def scroll_into_view( return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. - Appium iOS: Take driver screenshot and crop manually element from it + Appium iOS: Take driver screenshot and crop manually element from it. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -495,6 +406,7 @@ def get_elements_count(self, silent: bool = False) -> int: return len(self.all_elements) + @retry(AttributeError) def get_rect(self) -> dict: """ Retrieve the size and position of the element as a dictionary. @@ -505,6 +417,7 @@ def get_rect(self) -> dict: return dict(sorted_items) @property + @retry(TypeError) def size(self) -> Size: """ Get the size of the current element, including width and height. @@ -515,6 +428,7 @@ def size(self) -> Size: return Size(width=box['width'], height=box['height']) @property + @retry(TypeError) def location(self) -> Location: """ Get the location of the current element, including the x and y coordinates. @@ -549,7 +463,7 @@ def is_checked(self) -> bool: def _get_base(self) -> Union[PlaywrightPage, Locator]: """ - Get driver depends on parent element if available + Get driver depends on parent element if available. :return: driver """ @@ -565,7 +479,7 @@ def _get_base(self) -> Union[PlaywrightPage, Locator]: @property def _first_element(self): """ - Get first element + Get first element. :return: first element """ diff --git a/mops/selenium/core/core_driver.py b/mops/selenium/core/core_driver.py index 99513f7c..b7466026 100644 --- a/mops/selenium/core/core_driver.py +++ b/mops/selenium/core/core_driver.py @@ -1,26 +1,26 @@ from __future__ import annotations -import time from functools import cached_property -from typing import Union, List, Any, TYPE_CHECKING +import time +from typing import TYPE_CHECKING, Any, List, Optional, Union -from PIL import Image -from appium.webdriver.webdriver import WebDriver as AppiumDriver +from selenium.common.exceptions import NoAlertPresentException, WebDriverException as SeleniumWebDriverException +from mops.abstraction.driver_wrapper_abc import DriverWrapperABC +from mops.exceptions import DriverWrapperException, TimeoutException from mops.js_scripts import get_inner_height_js, get_inner_width_js from mops.mixins.objects.size import Size -from mops.shared_utils import _scaled_screenshot -from selenium.common.exceptions import WebDriverException as SeleniumWebDriverException, NoAlertPresentException -from selenium.webdriver.common.alert import Alert -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver - -from mops.abstraction.driver_wrapper_abc import DriverWrapperABC from mops.selenium.sel_utils import ActionChains -from mops.exceptions import DriverWrapperException, TimeoutException +from mops.shared_utils import _scaled_screenshot from mops.utils.internal_utils import WAIT_EL, WAIT_UNIT from mops.utils.logs import Logging if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver as AppiumDriver + from PIL import Image + from selenium.webdriver.common.alert import Alert + from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver + from mops.base.element import Element @@ -31,7 +31,7 @@ class CoreDriver(Logging, DriverWrapperABC): def __init__(self, driver: Union[AppiumDriver, SeleniumWebDriver]): """ Initializing of core driver - Contain same methods/data for both WebDriver and MobileDriver classes + Contain same methods/data for both WebDriver and MobileDriver classes. :param driver: appium or selenium driver to initialize """ @@ -72,7 +72,7 @@ def get_inner_window_size(self) -> Size: """ return Size( height=self.execute_script(get_inner_height_js), - width=self.execute_script(get_inner_width_js) + width=self.execute_script(get_inner_width_js), ) def get_window_size(self) -> Size: @@ -89,7 +89,7 @@ def get_window_size(self) -> Size: """ return Size(**self.driver.get_window_size()) - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> CoreDriver: + def wait(self, timeout: float = WAIT_UNIT) -> CoreDriver: """ Pauses the execution for a specified amount of time. @@ -117,14 +117,15 @@ def get(self, url: str, silent: bool = False) -> CoreDriver: try: self.driver.get(url) except SeleniumWebDriverException as exc: - raise DriverWrapperException(f'Can\'t proceed to {url}. Original error: {exc.msg}') + msg = f"Can't proceed to {url}. Original error: {exc.msg}" + raise DriverWrapperException(msg) return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web page. - Appium iOS: Removes native controls from image manually + Appium iOS: Removes native controls from image manually. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -149,7 +150,7 @@ def is_driver_opened(self) -> bool: :return: :obj:`bool` - :obj:`True` if the driver is open, otherwise :obj:`False`. """ - return True if self.driver else False + return bool(self.driver) def is_driver_closed(self) -> bool: """ @@ -157,7 +158,7 @@ def is_driver_closed(self) -> bool: :return: :obj:`bool` - :obj:`True` if the driver is closed, otherwise :obj:`False`. """ - return False if self.driver else True + return not self.driver @property def current_url(self) -> str: @@ -198,7 +199,7 @@ def go_back(self) -> CoreDriver: self.driver.back() return self - def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): + def quit(self, silent: bool = False, trace_path: str = 'trace.zip') -> None: """ Quit the driver instance. @@ -310,7 +311,7 @@ def set_page_load_timeout(self, timeout: int = 30) -> CoreDriver: self.driver.set_page_load_timeout(timeout) return self - def switch_to_alert(self, timeout: Union[int, float] = WAIT_EL) -> Alert: + def switch_to_alert(self, timeout: float = WAIT_EL) -> Alert: """ Appium/Selenium only: Wait for an alert and switch to it. @@ -328,7 +329,8 @@ def switch_to_alert(self, timeout: Union[int, float] = WAIT_EL) -> Alert: alert = None if not alert: - raise TimeoutException(f'Alert not found after {timeout} seconds') + msg = f'Alert not found after {timeout} seconds' + raise TimeoutException(msg) return alert diff --git a/mops/selenium/core/core_element.py b/mops/selenium/core/core_element.py index 70e870f2..f34acbe6 100644 --- a/mops/selenium/core/core_element.py +++ b/mops/selenium/core/core_element.py @@ -1,45 +1,44 @@ from __future__ import annotations -import time from abc import ABC -from typing import Union, List, Any, Callable, TYPE_CHECKING - -from PIL import Image +import time +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union -from mops.mixins.internal_mixin import get_element_info -from mops.mixins.objects.wait_result import Result -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver -from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement -from appium.webdriver.webelement import WebElement as AppiumWebElement -from selenium.webdriver.support.wait import WebDriverWait from selenium.common.exceptions import ( InvalidArgumentException as SeleniumInvalidArgumentException, InvalidSelectorException as SeleniumInvalidSelectorException, NoSuchElementException as SeleniumNoSuchElementException, - ElementNotInteractableException as SeleniumElementNotInteractableException, - ElementClickInterceptedException as SeleniumElementClickInterceptedException, StaleElementReferenceException as SeleniumStaleElementReferenceException, + WebDriverException as SeleniumWebDriverException, ) +from selenium.webdriver.support.wait import WebDriverWait + from mops.abstraction.element_abc import ElementABC -from mops.selenium.sel_utils import ActionChains -from mops.js_scripts import get_element_size_js, get_element_position_on_screen_js -from mops.keyboard_keys import KeyboardKeys -from mops.mixins.objects.location import Location -from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes, scroll_into_view_blocks -from mops.mixins.objects.size import Size -from mops.shared_utils import cut_log_data, _scaled_screenshot -from mops.utils.internal_utils import WAIT_EL, safe_call, get_dict, HALF_WAIT_EL, wait_condition, is_group from mops.exceptions import ( - TimeoutException, - InvalidSelectorException, DriverWrapperException, - NoSuchElementException, ElementNotInteractableException, + InvalidSelectorException, + NoSuchElementException, NoSuchParentException, ) +from mops.js_scripts import get_element_position_on_screen_js, get_element_size_js, hide_caret_js_script +from mops.mixins.internal_mixin import get_element_info +from mops.mixins.objects.location import Location +from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes, scroll_into_view_blocks +from mops.mixins.objects.size import Size +from mops.selenium.sel_utils import ActionChains +from mops.shared_utils import _scaled_screenshot, cut_log_data +from mops.utils.decorators import retry +from mops.utils.internal_utils import WAIT_EL, get_dict, is_group, safe_call if TYPE_CHECKING: + from appium.webdriver.webelement import WebElement as AppiumWebElement + from PIL import Image + from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver + from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement + from mops.base.element import Element + from mops.keyboard_keys import KeyboardKeys class CoreElement(ElementABC, ABC): @@ -54,16 +53,16 @@ class CoreElement(ElementABC, ABC): @property def element(self) -> SeleniumWebElement: """ - Get selenium WebElement object + Get selenium WebElement object. :return: SeleniumWebElement """ return self._get_element() @element.setter - def element(self, base_element: Union[SeleniumWebElement, AppiumWebElement]): + def element(self, base_element: Union[SeleniumWebElement, AppiumWebElement]) -> None: """ - Core element setter. Try to avoid usage of this function + Core element setter. Try to avoid usage of this function. :param base_element: selenium WebElement or appium WebElement """ @@ -80,6 +79,7 @@ def all_elements(self) -> Union[List[CoreElement], List[Any]]: # Element interaction + @retry(ElementNotInteractableException) def click(self, *, force_wait: bool = True, **kwargs) -> CoreElement: """ Clicks on the element. @@ -101,28 +101,22 @@ def click(self, *, force_wait: bool = True, **kwargs) -> CoreElement: """ self.log(f'Click into "{self.name}"') - self.element = self._get_element(force_wait=force_wait) - - selenium_exc_msg = None - start_time = time.time() - while time.time() - start_time < HALF_WAIT_EL: - try: - element = self.wait_enabled(silent=True).element - element.click() - return self - except ( - SeleniumElementNotInteractableException, - SeleniumElementClickInterceptedException, - SeleniumStaleElementReferenceException, - ) as exc: - selenium_exc_msg = exc.msg - finally: - self.element = None + if force_wait: + self.wait_visibility(silent=True) - raise ElementNotInteractableException( - f'Element "{self.name}" not interactable after {HALF_WAIT_EL} seconds. {self.get_element_info()}. ' + try: + self.wait_enabled(silent=True).element.click() + return self + except SeleniumWebDriverException as exc: + selenium_exc_msg = exc.msg + + msg = ( + f'Element "{self.name}" not interactable. {self.get_element_info()}. ' f'Original error: {selenium_exc_msg}' ) + raise ElementNotInteractableException( + msg, + ) def type_text(self, text: Union[str, KeyboardKeys], silent: bool = False) -> CoreElement: """ @@ -140,6 +134,7 @@ def type_text(self, text: Union[str, KeyboardKeys], silent: bool = False) -> Cor self.log(f'Type text "{cut_log_data(text)}" into "{self.name}"') self.element.send_keys(text) + return self def type_slowly(self, text: str, sleep_gap: float = 0.05, silent: bool = False) -> CoreElement: @@ -163,6 +158,7 @@ def type_slowly(self, text: str, sleep_gap: float = 0.05, silent: bool = False) for letter in str(text): element.send_keys(letter) time.sleep(sleep_gap) + return self def clear_text(self, silent: bool = False) -> CoreElement: @@ -177,6 +173,7 @@ def clear_text(self, silent: bool = False) -> CoreElement: self.log(f'Clear text in "{self.name}"') self.element.clear() + return self def check(self) -> CoreElement: @@ -211,102 +208,13 @@ def uncheck(self) -> CoreElement: return self - # Element waits - - @wait_condition - def wait_visibility(self, *, timeout: int = WAIT_EL, silent: bool = False) -> CoreElement: - """ - Waits until the element becomes visible. - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`CoreElement` - """ - return Result( # noqa - execution_result=self.is_displayed(silent=True), - log=f'Wait until "{self.name}" becomes visible', - exc=TimeoutException(f'"{self.name}" not visible', info=self) - ) - - @wait_condition - def wait_hidden(self, *, timeout: int = WAIT_EL, silent: bool = False) -> CoreElement: - """ - Waits until the element becomes hidden. - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`CoreElement` - """ - return Result( # noqa - execution_result=self.is_hidden(silent=True), - log=f'Wait until "{self.name}" becomes hidden', - exc=TimeoutException(f'"{self.name}" still visible', info=self), - ) - - @wait_condition - def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> CoreElement: - """ - Waits until the element becomes available in DOM tree. \n - **Note:** The method requires the use of named arguments. - - **Selenium:** - - - Applied :func:`wait_condition` decorator integrates a 0.1 seconds delay for each iteration - during the waiting process. - - **Appium:** - - - Applied :func:`wait_condition` decorator integrates an exponential delay - (starting at 0.1 seconds, up to a maximum of 1.6 seconds) which increases - with each iteration during the waiting process. - - :param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`. - :type timeout: int - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`CoreElement` - """ - return Result( # noqa - execution_result=self.is_available(), - log=f'Wait until presence of "{self.name}"', - exc=TimeoutException(f'"{self.name}" not available in DOM', info=self), - ) - # Element state def scroll_into_view( self, block: ScrollTo = ScrollTo.CENTER, behavior: ScrollTypes = ScrollTypes.INSTANT, - sleep: Union[int, float] = 0, + sleep: float = 0, silent: bool = False, ) -> CoreElement: """ @@ -334,10 +242,10 @@ def scroll_into_view( return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. - Appium iOS: Take driver screenshot and crop manually element from it + Appium iOS: Take driver screenshot and crop manually element from it. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -354,9 +262,12 @@ def screenshot_base(self) -> bytes: :return: :class:`bytes` - screenshot binary """ + self.execute_script(hide_caret_js_script) + return self.element.screenshot_as_png @property + @retry(SeleniumStaleElementReferenceException) def text(self) -> str: """ Returns the text of the element. @@ -410,14 +321,14 @@ def is_displayed(self, silent: bool = False) -> bool: :type silent: bool :return: :class:`bool` """ - if not silent: - self.log(f'Check displaying of "{self.name}"') - is_displayed = self.is_available() if is_displayed: is_displayed = safe_call(self._cached_element.is_displayed) + if not silent: + self.log(f'Check displaying of "{self.name}" - {is_displayed}') + return is_displayed def is_hidden(self, silent: bool = False) -> bool: @@ -428,11 +339,14 @@ def is_hidden(self, silent: bool = False) -> bool: :type silent: bool :return: :class:`bool` """ + status = not self.is_displayed(silent=True) + if not silent: - self.log(f'Check invisibility of "{self.name}"') + self.log(f'Check invisibility of "{self.name}" - {status}') - return not self.is_displayed(silent=True) + return status + @retry(SeleniumStaleElementReferenceException) def get_attribute(self, attribute: str, silent: bool = False) -> str: """ Retrieve a specific attribute from the current element. @@ -460,7 +374,8 @@ def get_all_texts(self, silent: bool = False) -> List[str]: self.log(f'Get all texts from "{self.name}"') self.wait_visibility(silent=True) - return list(element_item.text for element_item in self.all_elements) + + return [element_item.text for element_item in self.all_elements] def get_elements_count(self, silent: bool = False) -> int: """ @@ -485,6 +400,7 @@ def get_rect(self) -> dict: return dict(sorted_items) @property + @retry(SeleniumStaleElementReferenceException) def size(self) -> Size: """ Get the size of the current element, including width and height. @@ -494,6 +410,7 @@ def size(self) -> Size: return Size(**self.execute_script(get_element_size_js)) @property + @retry(SeleniumStaleElementReferenceException) def location(self) -> Location: """ Get the location of the current element, including the x and y coordinates. @@ -527,7 +444,7 @@ def is_checked(self) -> bool: def _get_wait(self, timeout: int = WAIT_EL) -> WebDriverWait: """ - Get wait with depends on parent element if available + Get wait with depends on parent element if available. :return: driver """ @@ -536,7 +453,7 @@ def _get_wait(self, timeout: int = WAIT_EL) -> WebDriverWait: @property def _action_chains(self) -> ActionChains: """ - Get action chains with depends on parent element if available + Get action chains with depends on parent element if available. :return: ActionChains """ @@ -544,7 +461,7 @@ def _action_chains(self) -> ActionChains: def _get_element(self, wait: Union[bool, Callable] = True, force_wait: bool = False) -> SeleniumWebElement: """ - Get selenium element from driver or parent element + Get selenium element from driver or parent element. :param wait: wait strategy for element and/or element parent before grab :param force_wait: force wait for some element @@ -568,33 +485,39 @@ def _get_element(self, wait: Union[bool, Callable] = True, force_wait: bool = Fa if not element: element_info = f'"{self.name}" {self.__class__.__name__}' if self.parent and not self._get_cached_element(self.parent): - raise NoSuchParentException( + msg = ( f'{self._get_container_info()} container not found while accessing {element_info}. ' f'{get_element_info(self.parent, "Container Selector=")}' ) + raise NoSuchParentException( + msg, + ) - raise NoSuchElementException( + msg = ( f'Unable to locate the {element_info}. ' f'{self.get_element_info()}' f'{self._ensure_unique_parent()}' ) + raise NoSuchElementException( + msg, + ) return element def _get_base(self, wait: Union[bool, Callable] = True) -> Union[SeleniumWebDriver, SeleniumWebElement]: """ - Get driver with depends on parent element if available + Get driver with depends on parent element if available. :return: driver """ base = self.driver if not base: - raise DriverWrapperException("Can't find driver") + msg = "Can't find driver" + raise DriverWrapperException(msg) - if self.driver_wrapper.is_appium: - if self.driver_wrapper.is_native_context: - return base + if self.driver_wrapper.is_appium and self.driver_wrapper.is_native_context: + return base if self.parent: base = self.parent._get_element(wait=wait) @@ -603,7 +526,7 @@ def _get_base(self, wait: Union[bool, Callable] = True) -> Union[SeleniumWebDriv def _find_element(self, wait_parent: bool = False) -> Union[SeleniumWebElement, AppiumWebElement]: """ - Find selenium/appium element + Find selenium/appium element. :param wait_parent: wait for base(parent) element :return: SeleniumWebElement or AppiumWebElement @@ -622,7 +545,7 @@ def _find_element(self, wait_parent: bool = False) -> Union[SeleniumWebElement, def _find_elements(self, wait_parent: bool = False) -> List[Union[SeleniumWebElement, AppiumWebElement]]: """ - Find all selenium/appium elements + Find all selenium/appium elements. :param wait_parent: wait for base(parent) element :return: list of SeleniumWebElement or AppiumWebElement @@ -642,7 +565,7 @@ def _find_elements(self, wait_parent: bool = False) -> List[Union[SeleniumWebEle def _raise_invalid_selector_exception(self, exc: Any) -> None: """ - Raises InvalidSelectorException if specific keywords in exception message + Raises InvalidSelectorException if specific keywords in exception message. :param exc: original exc object :return: None @@ -650,8 +573,7 @@ def _raise_invalid_selector_exception(self, exc: Any) -> None: if 'invalid locator' in exc.msg or 'is not a valid' in exc.msg: msg = f'Selector for "{self.name}" is invalid. {self.get_element_info()}' raise InvalidSelectorException(msg) - else: - raise exc + raise exc def _get_container_info(self) -> str: container_info = f'"{self.parent.name}"' @@ -662,7 +584,7 @@ def _get_container_info(self) -> str: def _ensure_unique_parent(self) -> str: """ - Ensure that parent is unique and give information if it isn't + Ensure that parent is unique and give information if it isn't. :return: empty string or warning info """ @@ -676,7 +598,7 @@ def _ensure_unique_parent(self) -> str: def _get_cached_element(self, obj: Union[CoreElement, Element]) -> Union[None, SeleniumWebElement, AppiumWebElement]: """ - Get cached element from given object + Get cached element from given object. :param obj: CoreElement object :return: None, SeleniumWebElement, AppiumWebElement diff --git a/mops/selenium/driver/mobile_driver.py b/mops/selenium/driver/mobile_driver.py index c72734d2..621dd5d0 100644 --- a/mops/selenium/driver/mobile_driver.py +++ b/mops/selenium/driver/mobile_driver.py @@ -1,12 +1,14 @@ from __future__ import annotations -from typing import Union, List, Optional +from typing import TYPE_CHECKING, List, Optional, Union from appium.webdriver.applicationstate import ApplicationState -from appium.webdriver.webdriver import WebDriver as AppiumDriver -from mops.selenium.core.core_driver import CoreDriver from mops.mixins.native_context import NativeContext, NativeSafari +from mops.selenium.core.core_driver import CoreDriver + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver as AppiumDriver class MobileDriver(CoreDriver): @@ -15,7 +17,7 @@ class MobileDriver(CoreDriver): def __init__(self, driver_container: Driver, *args, **kwargs): # noqa """ - Initializing of mobile driver with appium + Initializing of mobile driver with appium. :param driver_container: Driver that contains appium driver object """ @@ -132,6 +134,7 @@ def get_web_view_context(self) -> Union[None, str]: for context in self.get_all_contexts(): if 'WEBVIEW' in context: return context + return None def get_current_context(self) -> str: """ @@ -168,7 +171,7 @@ def is_web_context(self) -> bool: @property def top_bar_height(self) -> int: """ - iOS only - Get the height of the top bar. + IOS only - Get the height of the top bar. :return: :obj:`int` - The height of the top bar in pixels. """ @@ -181,7 +184,7 @@ def top_bar_height(self) -> int: @property def bottom_bar_height(self) -> int: """ - iOS only - Get the height of the bottom bar. + IOS only - Get the height of the bottom bar. :return: :obj:`int` - The height of the bottom bar in pixels. """ @@ -203,10 +206,10 @@ def get_all_contexts(self) -> List[str]: """ return self.driver.contexts - def screenshot_image(self, screenshot_base: bytes = None): + def screenshot_image(self, screenshot_base: Optional[bytes] = None): """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web page. - Appium iOS: Removes native controls from image manually + Appium iOS: Removes native controls from image manually. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -266,7 +269,7 @@ def click_by_coordinates(self, x: int, y: int, silent: bool = False) -> MobileDr def _set_static(obj) -> None: """ - Set static attributes for Appium driver wrapper + Set static attributes for Appium driver wrapper. :return: None """ @@ -290,4 +293,5 @@ def _set_static(obj) -> None: elif obj.is_android: obj.bundle_id = obj.caps.get('appPackage', 'undefined: appPackage') else: - raise Exception('Make sure that correct "platformName" capability specified') + msg = 'Make sure that correct "platformName" capability specified' + raise Exception(msg) diff --git a/mops/selenium/driver/web_driver.py b/mops/selenium/driver/web_driver.py index 79338e3b..ee584219 100644 --- a/mops/selenium/driver/web_driver.py +++ b/mops/selenium/driver/web_driver.py @@ -1,20 +1,22 @@ from __future__ import annotations from dataclasses import astuple -from typing import List +from typing import TYPE_CHECKING, List -from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver - -from mops.mixins.objects.size import Size from mops.selenium.core.core_driver import CoreDriver -from mops.mixins.objects.driver import Driver + +if TYPE_CHECKING: + from selenium.webdriver.remote.webdriver import WebDriver as SeleniumWebDriver + + from mops.mixins.objects.driver import Driver + from mops.mixins.objects.size import Size class WebDriver(CoreDriver): def __init__(self, driver_container: Driver, *args, **kwargs): # noqa """ - Initializing of desktop web driver with selenium + Initializing of desktop web driver with selenium. :param driver_container: Driver that contains selenium driver object """ @@ -76,10 +78,7 @@ def switch_to_tab(self, tab: int = -1) -> WebDriver: :type tab: int :return: :obj:`.WebDriver` - The current instance of the driver wrapper, now switched to the specified tab. """ - if tab == -1: - tab = self.get_all_tabs()[tab] - else: - tab = self.get_all_tabs()[tab - 1] + tab = self.get_all_tabs()[tab] if tab == -1 else self.get_all_tabs()[tab - 1] self.driver.switch_to.window(tab) return self diff --git a/mops/selenium/elements/mobile_element.py b/mops/selenium/elements/mobile_element.py index a4d8dbc8..13a96a2c 100644 --- a/mops/selenium/elements/mobile_element.py +++ b/mops/selenium/elements/mobile_element.py @@ -1,23 +1,23 @@ from __future__ import annotations -import time from abc import ABC +import time +from typing import TYPE_CHECKING, Optional -from PIL.Image import Image - -from mops.selenium.core.core_element import CoreElement from mops.mixins.objects.location import Location from mops.mixins.objects.size import Size +from mops.selenium.core.core_element import CoreElement from mops.utils.internal_utils import calculate_coordinate_to_click -from mops.utils.selector_synchronizer import get_platform_locator, set_selenium_selector, set_appium_selector +from mops.utils.selector_synchronizer import get_platform_locator, set_appium_selector + +if TYPE_CHECKING: + from PIL.Image import Image class MobileElement(CoreElement, ABC): def __init__(self): - """ - Initializing of mobile element with appium driver - """ + """Initializing of mobile element with appium driver.""" self.locator = get_platform_locator(self) set_appium_selector(self) @@ -31,9 +31,8 @@ def click_outside(self, x: int = -5, y: int = -5) -> MobileElement: :type y: int :return: :class:`MobileElement` """ - if self.driver_wrapper.is_web_context: - if not self.is_fully_visible(silent=True): - self.scroll_into_view() + if self.driver_wrapper.is_web_context and not self.is_fully_visible(silent=True): + self.scroll_into_view() x, y = calculate_coordinate_to_click(self, x, y) @@ -53,9 +52,8 @@ def click_into_center(self, silent: bool = False) -> MobileElement: :type silent: bool :return: :class:`MobileElement` """ - if self.driver_wrapper.is_web_context: - if not self.is_fully_visible(silent=True): - self.scroll_into_view() + if self.driver_wrapper.is_web_context and not self.is_fully_visible(silent=True): + self.scroll_into_view() x, y = calculate_coordinate_to_click(self, 0, 0) @@ -112,10 +110,10 @@ def click_in_alert(self) -> MobileElement: return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: + def screenshot_image(self, screenshot_base: Optional[bytes] = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. - Appium iOS: Take driver screenshot and crop manually element from it + Appium iOS: Take driver screenshot and crop manually element from it. :param screenshot_base: Screenshot binary data (optional). If :obj:`None` is provided then takes a new screenshot @@ -161,7 +159,7 @@ def location(self) -> Location: def _element_box(self) -> tuple: """ - Get element coordinates on screen for ios safari + Get element coordinates on screen for ios safari. :return: element coordinates on screen (start_x, start_y, end_x, end_y) """ diff --git a/mops/selenium/elements/web_element.py b/mops/selenium/elements/web_element.py index 5ab487a7..fffad116 100644 --- a/mops/selenium/elements/web_element.py +++ b/mops/selenium/elements/web_element.py @@ -1,9 +1,10 @@ from __future__ import annotations from abc import ABC +from typing import Any -from mops.selenium.core.core_element import CoreElement from mops.js_scripts import js_click +from mops.selenium.core.core_element import CoreElement from mops.utils.internal_utils import calculate_coordinate_to_click from mops.utils.selector_synchronizer import get_platform_locator, set_selenium_selector @@ -11,13 +12,11 @@ class WebElement(CoreElement, ABC): def __init__(self): - """ - Initializing of web element with selenium driver - """ + """Initialize of web element with selenium driver""" self.locator = get_platform_locator(self) set_selenium_selector(self) - def click(self, *, force_wait: bool = True, **kwargs) -> WebElement: + def click(self, *, force_wait: bool = True, **kwargs: Any) -> WebElement: """ Clicks on the element. diff --git a/mops/selenium/pages/mobile_page.py b/mops/selenium/pages/mobile_page.py index 59602710..e92be94f 100644 --- a/mops/selenium/pages/mobile_page.py +++ b/mops/selenium/pages/mobile_page.py @@ -1,8 +1,7 @@ from __future__ import annotations -import time from abc import ABC -from typing import Union +import time from mops.selenium.core.core_page import CorePage @@ -16,7 +15,7 @@ def swipe( end_x: int, end_y: int, duration: int = 0, - sleep: Union[int, float] = 0 + sleep: float = 0, ) -> MobilePage: """ Appium only: Swipe from one point to another, with an optional duration and post-swipe delay. diff --git a/mops/selenium/sel_utils.py b/mops/selenium/sel_utils.py index 41a1752d..5b22314f 100644 --- a/mops/selenium/sel_utils.py +++ b/mops/selenium/sel_utils.py @@ -7,7 +7,7 @@ class ActionChains(SeleniumActionChains): def move_to_location(self, x: int, y: int) -> ActionChains: """ - Moving the mouse to specified location + Moving the mouse to specified location. :param x: x coordinate :param y: y coordinate diff --git a/mops/shared_utils.py b/mops/shared_utils.py index 687064e3..358bf911 100644 --- a/mops/shared_utils.py +++ b/mops/shared_utils.py @@ -1,13 +1,13 @@ import io import logging -from subprocess import Popen, PIPE, run +from subprocess import PIPE, Popen, run from PIL import Image def _scaled_screenshot(screenshot_binary: bytes, width: int) -> Image: """ - Get scaled screenshot to fit driver window / element size + Get scaled screenshot to fit driver window / element size. :param screenshot_binary: original screenshot binary :param width: driver or element width @@ -55,7 +55,7 @@ def shell_running_command(cmd, **kwargs): def shell_command(cmd, **kwargs): - process = run(cmd, shell=True, **kwargs) + process = run(cmd, shell=True, **kwargs, check=False) if process.stdout: process.output = process.stdout.decode('utf8').replace('\n', '') @@ -69,7 +69,7 @@ def shell_command(cmd, **kwargs): def cut_log_data(data: str, length=50) -> str: """ - Cut given data for reducing log length + Cut given data for reducing log length. :param data: original data ~ 'very long string for typing. string endless continues' :param length: length to cut given data ~ 20 @@ -81,7 +81,7 @@ def cut_log_data(data: str, length=50) -> str: def disable_logging(loggers: list) -> None: """ - Disable logging for given loggers + Disable logging for given loggers. :param loggers: list of loggers to be disabled :return: None @@ -92,7 +92,7 @@ def disable_logging(loggers: list) -> None: def set_log_level(loggers: list, level: int) -> None: """ - Set log level for given loggers + Set log level for given loggers. :param loggers: list of loggers to be disabled :param level: level to be set diff --git a/mops/utils/decorators.py b/mops/utils/decorators.py new file mode 100644 index 00000000..e51e4e5a --- /dev/null +++ b/mops/utils/decorators.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from functools import wraps +import time +from typing import TYPE_CHECKING, Any, Callable, Union + +from mops.exceptions import ContinuousWaitException +from mops.utils.internal_utils import ( + HALF_WAIT_EL, + QUARTER_WAIT_EL, + WAIT_EL, + WAIT_METHODS_DELAY, + increase_delay, + validate_silent, + validate_timeout, +) +from mops.utils.logs import LogLevel, autolog + +if TYPE_CHECKING: + from mops.base.element import Element + from mops.mixins.objects.wait_result import Result + + +def retry(exceptions, timeout: int = HALF_WAIT_EL): + """ + A decorator to retry a function when specified exceptions occur. + + :param exceptions: Exception or tuple of exception classes to catch and retry on. + :param timeout: The maximum time (in seconds) to keep retrying before giving up. + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + timestamp = None + + while True: + try: + return func(*args, **kwargs) + except exceptions as exc: + if not timestamp: + timestamp = time.time() + elif time.time() - timestamp >= timeout: + raise + autolog( + f'Caught "{exc.__class__.__name__}" while executing "{func.__name__}", retrying...', + level=LogLevel.WARNING, + ) + return wrapper + return decorator + + +def wait_condition(method: Callable): + + @wraps(method) + def wrapper( + self: Element, + *args: Any, + timeout: float = WAIT_EL, + silent: bool = False, + continuous: bool = False, + **kwargs: Any, + ): + validate_timeout(timeout) + validate_silent(silent) + + should_increase_delay = self.driver_wrapper.is_appium + delay = WAIT_METHODS_DELAY + is_log_needed = not silent + start_time = time.time() + + if continuous: + return method(self, *args, **kwargs) + + while time.time() - start_time < timeout: + result: Result = method(self, *args, **kwargs) + + if is_log_needed: + self.log(result.log) + is_log_needed = False + + if result.execution_result: + return self + + time.sleep(delay) + + if should_increase_delay: + delay = increase_delay(delay) + + result.exc._timeout = timeout # noqa + raise result.exc + + return wrapper + + +def wait_continuous(method: Callable): + + @wraps(method) + def wrapper( + self: Element, + *args: Any, + silent: bool = False, + continuous: Union[float, bool] = False, + **kwargs: Any, + ): + result: Element = method(self, *args, silent=silent, continuous=False, **kwargs) # Wait for initial condition + + if not continuous: + return result + + should_increase_delay = self.driver_wrapper.is_appium + delay = WAIT_METHODS_DELAY + start_time = time.time() + is_log_needed = not silent + timeout = continuous if type(continuous) in (int, float) else QUARTER_WAIT_EL + + while time.time() - start_time < timeout: + result: Result = method(self, *args, silent=silent, continuous=True, **kwargs) + + if is_log_needed: + self.log(f'Starting continuous "{method.__name__}" for the "{self.name}" for next {timeout} seconds') + is_log_needed = False + + if not result.execution_result: + msg = ( + f'The continuous "{method.__name__}" of the "{self.name}" is no met ' + f'after {(time.time() - start_time):.2f} seconds' + ) + raise ContinuousWaitException( + msg, + ) + + time.sleep(delay) + + if should_increase_delay: + delay = increase_delay(delay) + + return self + + return wrapper diff --git a/mops/utils/internal_utils.py b/mops/utils/internal_utils.py index c43b68a4..ab7a3aab 100644 --- a/mops/utils/internal_utils.py +++ b/mops/utils/internal_utils.py @@ -1,18 +1,19 @@ from __future__ import annotations -import sys -import inspect -import time from copy import copy -from functools import lru_cache, wraps -from typing import Any, Union, Callable +from functools import lru_cache +import inspect +import sys +from typing import TYPE_CHECKING, Any, Callable, Optional, Union -from mops.mixins.objects.size import Size -from mops.mixins.objects.wait_result import Result from selenium.common.exceptions import StaleElementReferenceException as SeleniumStaleElementReferenceException -from mops.exceptions import NoSuchElementException, InvalidSelectorException, TimeoutException, NoSuchParentException +from mops.exceptions import InvalidSelectorException, NoSuchElementException, NoSuchParentException, TimeoutException +if TYPE_CHECKING: + from mops.base.element import Element + from mops.base.group import Group + from mops.mixins.objects.size import Size WAIT_METHODS_DELAY = 0.1 WAIT_UNIT = 1 @@ -24,16 +25,16 @@ all_tags = {'h1', 'h2', 'h3', 'h4', 'h5', 'head', 'body', 'input', 'section', 'button', 'a', 'link', 'header', 'div', 'textarea', 'svg', 'circle', 'iframe', 'label', 'p', 'tr', 'th', 'table', 'tbody', 'td', 'select', 'nav', - 'li', 'form', 'footer', 'frame', 'area', 'span'} + 'li', 'form', 'footer', 'frame', 'area', 'span', 'video'} -def get_dict(obj: Any): +def get_dict(obj: Any) -> dict: return obj.__dict__ -def safe_call(func: Callable, *args, **kwargs) -> Union[Any, None]: +def safe_call(func: Callable, *args: Any, **kwargs: Any) -> Union[Any, None]: """ - Wrapper for any method that raises internal exceptions to prevent exceptions + Wrapper for any method that raises internal exceptions to prevent exceptions. :param func: any internal function :param args: any args for function @@ -55,9 +56,9 @@ def safe_call(func: Callable, *args, **kwargs) -> Union[Any, None]: @lru_cache(maxsize=None) -def get_timeout_in_ms(timeout: Union[int, float]): +def get_timeout_in_ms(timeout: float) -> float: """ - Get timeout in milliseconds for playwright + Get timeout in milliseconds for playwright. :param timeout: timeout in seconds :return: timeout in milliseconds @@ -65,18 +66,18 @@ def get_timeout_in_ms(timeout: Union[int, float]): return validate_timeout(timeout) * 1000 -def safe_getattribute(obj, item): +def safe_getattribute(obj: Any, item: str): return object.__getattribute__(obj, item) -def get_frame(frame=1): +def get_frame(frame: int = 1): """ - Get frame by given id + Get frame by given id. :param frame: frame id, "current" by default :return: frame """ - return sys._getframe(frame) # noqa + return sys._getframe(frame) # noqa: def is_element(obj: Any) -> bool: @@ -99,9 +100,9 @@ def is_driver_wrapper(obj: Any) -> bool: return getattr(obj, '_object', None) == 'driver_wrapper' -def initialize_objects(current_object, objects: dict, cls: Any): +def initialize_objects(current_object: Union[Group, Element], objects: dict, cls: Any) -> None: """ - Copy objects and initializing them with driver_wrapper from current object + Copy objects and initializing them with driver_wrapper from current object. :param current_object: list of objects to initialize :param objects: list of objects to initialize @@ -115,11 +116,11 @@ def initialize_objects(current_object, objects: dict, cls: Any): initialize_objects(copied_obj, get_child_elements_with_names(copied_obj, cls), cls) -def set_parent_for_attr(base_obj: object, instance_class: Union[type, tuple], with_copy: bool = False): +def set_parent_for_attr(base_obj: object, instance_class: Union[type, tuple], with_copy: bool = False) -> None: """ - Sets parent for all Elements/Group of given class. + Set parent for all Elements/Group of given class. Should be called ONLY in Group object or all_elements method. - Copy of objects will be executed if with_copy is True. Required for all_elements method + Copy of objects will be executed if with_copy is True. Required for all_elements method. :param instance_class: attribute class to looking for :param base_obj: object of attribute @@ -141,9 +142,9 @@ def set_parent_for_attr(base_obj: object, instance_class: Union[type, tuple], wi set_parent_for_attr(child, instance_class, with_copy) -def promote_parent_element(obj: Any, base_obj: Any, cls: Any): +def promote_parent_element(obj: Any, base_obj: Any, cls: Any) -> None: """ - Promote parent object in Element if parent is another Element + Promote parent object in Element if parent is another Element. :param obj: any element :param base_obj: base object of element: Page/Group instance @@ -153,7 +154,7 @@ def promote_parent_element(obj: Any, base_obj: Any, cls: Any): initial_parent = getattr(obj, 'parent', None) if not initial_parent: - return None + return if is_element_instance(initial_parent) and initial_parent != base_obj: for el in get_child_elements(base_obj, cls): @@ -163,33 +164,34 @@ def promote_parent_element(obj: Any, base_obj: Any, cls: Any): def get_child_elements(obj: object, instance: Union[type, tuple]) -> list: """ - Return objects of this object by instance + Return objects of this object by instance. :returns: list of page elements and page objects """ return list(get_child_elements_with_names(obj, instance).values()) -def get_child_elements_with_names(obj: Any, instance: Union[type, tuple] = None) -> dict: +def get_child_elements_with_names(obj: Any, instance: Optional[Union[type, tuple]] = None) -> dict: """ Return all objects of given object or by instance - Removing parent attribute from list to avoid infinite recursion and all dunder attributes + Removing parent attribute from list to avoid infinite recursion and all dunder attributes. :returns: dict of page elements and page objects """ elements = {} for attribute, value in get_all_attributes_from_object(obj).items(): - if instance and isinstance(value, instance) or not instance: - if attribute != 'parent' and not attribute.startswith('__') and not attribute.endswith('__'): - elements.update({attribute: value}) + if (not instance or not isinstance(value, instance)) and instance: + continue + if attribute != 'parent' and not attribute.startswith('__') and not attribute.endswith('__'): + elements.update({attribute: value}) return elements def get_all_attributes_from_object(reference_obj: Any) -> dict: """ - Get attributes from given object and all its bases + Get attributes from given object and all its bases. :param reference_obj: reference object :return: dict of all attributes @@ -205,7 +207,7 @@ def get_all_attributes_from_object(reference_obj: Any) -> dict: for parent_class in all_bases: - if 'ABC' in str(parent_class) or parent_class == object: + if 'ABC' in str(parent_class) or parent_class is object: continue items.update(dict(parent_class.__dict__)) @@ -215,7 +217,7 @@ def get_all_attributes_from_object(reference_obj: Any) -> dict: def get_attributes_from_object(reference_obj: Any) -> dict: """ - Get attributes from given object + Get attributes from given object. :param reference_obj: :return: @@ -233,12 +235,12 @@ def get_attributes_from_object(reference_obj: Any) -> dict: return items -def is_target_on_screen(x: int, y: int, possible_range: Size): +def is_target_on_screen(x: int, y: int, possible_range: Size) -> bool: """ Check is given coordinates fit into given range An safe value will be applied: 1 - Due to usage of range - 2 - Due to rounding a number when get size/location of element + 2 - Due to rounding a number when get size/location of element. :param x: x coordinate :param y: y coordinate @@ -258,7 +260,7 @@ def calculate_coordinate_to_click(element: Any, x: int = 0, y: int = 0) -> tuple (0, 0) -- center of the element (5, 0) -- 5 pixels to the right (-10, 0) -- 10 pixels to the left out of the element - (0, -5) -- 5 pixels below the element + (0, -5) -- 5 pixels below the element. :param element: mops WebElement or MobileElement :param x: horizontal offset relative to either left (x < 0) or right side (x > 0) @@ -276,55 +278,27 @@ def calculate_coordinate_to_click(element: Any, x: int = 0, y: int = 0) -> tuple return int(x), int(y) -def validate_timeout(timeout) -> Union[float, int]: +def validate_timeout(timeout: float) -> Union[float, int]: if type(timeout) not in (int, float): - raise TypeError('The type of `timeout` arg must be int or float') + msg = 'The type of `timeout` arg must be int or float' + raise TypeError(msg) if timeout <= 0: - raise ValueError('The `timeout` value must be a positive number') + msg = 'The `timeout` value must be a positive number' + raise ValueError(msg) return timeout -def validate_silent(silent) -> bool: +def validate_silent(silent: bool) -> bool: if not isinstance(silent, bool): - raise TypeError(f'The type of `silent` arg must be bool') + msg = 'The type of `silent` arg must be bool' + raise TypeError(msg) return silent -def increase_delay(delay, max_delay: Union[int, float] = 1.5) -> Union[int, float]: +def increase_delay(delay: float, max_delay: float = 1.5) -> Union[int, float]: if delay < max_delay: return delay + delay return delay - - -def wait_condition(method: Callable): - - @wraps(method) - def wrapper(self, *args, timeout: Union[int, float] = WAIT_EL, silent: bool = False, **kwargs): - validate_timeout(timeout) - validate_silent(silent) - - start_time = time.time() - result: Result = method(self, *args, **kwargs) - - if not silent: - self.log(result.log) - - should_increase_delay = self.driver_wrapper.is_appium - delay = WAIT_METHODS_DELAY - - while time.time() - start_time < timeout and not result.execution_result: - time.sleep(delay) - result: Result = method(self, *args, **kwargs) - if should_increase_delay: - delay = increase_delay(delay) - - if result.execution_result: - return self - - result.exc._timeout = timeout # noqa - raise result.exc - - return wrapper diff --git a/mops/utils/logs.py b/mops/utils/logs.py index 65f615bf..c4a9faba 100644 --- a/mops/utils/logs.py +++ b/mops/utils/logs.py @@ -1,14 +1,13 @@ from __future__ import annotations -import logging -import sys from functools import lru_cache +import logging from os.path import basename +import sys from typing import Any from mops.utils.internal_utils import get_frame, is_driver_wrapper - logger = logging.getLogger('mops') @@ -22,7 +21,7 @@ class LogLevel: def driver_wrapper_logs_settings(level: str = LogLevel.INFO) -> None: """ - Sets driver wrapper log format(unchangeable) and log level (can be changed) + Set driver wrapper log format(unchangeable) and log level (can be changed). :param level: log level to be captured. Example: DEBUG - all, CRITICAL - only highest level priority level :return: None @@ -33,14 +32,14 @@ def driver_wrapper_logs_settings(level: str = LogLevel.INFO) -> None: handler.setLevel(level) handler.setFormatter(logging.Formatter( fmt='[%(asctime)s.%(msecs)03d][%(levelname).1s]%(message)s', - datefmt="%h %d][%H:%M:%S" + datefmt='%h %d][%H:%M:%S', )) logger.addHandler(handler) def autolog(message: Any, level: str = LogLevel.INFO) -> None: """ - Logs a message with detailed context in the following format: + Log a message with detailed context in the following format: .. code-block:: text @@ -62,7 +61,7 @@ class Logging: def log(self: Any, message: str, level: str = LogLevel.INFO) -> None: """ - Logs a message with detailed context in the following format: + Log a message with detailed context in the following format: .. code-block:: text @@ -77,17 +76,13 @@ def log(self: Any, message: str, level: str = LogLevel.INFO) -> None: :type level: str :return: :obj:`None` """ - if is_driver_wrapper(self): - label = self.label - else: - label = self.driver_wrapper.label + label = self.label if is_driver_wrapper(self) else self.driver_wrapper.label _send_log_message(f'[{label}]{self._get_code_info()} {message}', level) - return None def _get_code_info(self) -> str: """ - Get executed code info: filename/function name/line + Get executed code info: filename/function name/line. :return: log message """ @@ -97,7 +92,7 @@ def _get_code_info(self) -> str: def _send_log_message(log_message: str, level: str) -> None: """ - Send log message + Send log message. :param level: log level :param log_message: custom message @@ -109,7 +104,7 @@ def _send_log_message(log_message: str, level: str) -> None: @lru_cache(maxsize=None) def _get_log_level(level: str) -> int: """ - Get log level from string. Moved to a different function for using @lru_cache + Get log level from string. Moved to a different function for using @lru_cache. :param level: string level ~ INFO, DEBUG etc. :return: log level in int format diff --git a/mops/utils/previous_object_driver.py b/mops/utils/previous_object_driver.py index 6d4ec6c6..c24a8ef9 100644 --- a/mops/utils/previous_object_driver.py +++ b/mops/utils/previous_object_driver.py @@ -5,15 +5,17 @@ from mops.base.driver_wrapper import DriverWrapperSessions +MULTIPLE_DRIVERS_COUNT = 2 + def set_instance_frame(new_instance: Any) -> None: """ - Sets frame on element initialisation + Set frame on element initialisation. :param new_instance: object instance from __new__ :return: None """ - if DriverWrapperSessions.sessions_count() >= 2: + if DriverWrapperSessions.sessions_count() >= MULTIPLE_DRIVERS_COUNT: frame = inspect.currentframe() while frame.f_code.co_name != '__new__': @@ -26,20 +28,22 @@ class PreviousObjectDriver: def set_driver_from_previous_object(self, current_obj: Any) -> None: """ - Set driver for given object from previous object + Set driver for given object from previous object. :param current_obj: element object :return: None """ - if len(DriverWrapperSessions.all_sessions) >= 2: - if current_obj.driver_wrapper == DriverWrapperSessions.first_session(): - previous_object = self._get_prev_obj_instance(current_obj=current_obj) - if previous_object and getattr(previous_object, 'driver_wrapper', None): - current_obj.driver_wrapper = previous_object.driver_wrapper + if DriverWrapperSessions.sessions_count() < MULTIPLE_DRIVERS_COUNT: + return + + if current_obj.driver_wrapper == DriverWrapperSessions.first_session(): + previous_object = self._get_prev_obj_instance(current_obj=current_obj) + if previous_object and getattr(previous_object, 'driver_wrapper', None): + current_obj.driver_wrapper = previous_object.driver_wrapper def _get_prev_obj_instance(self, current_obj: Any) -> Union[None, Any]: """ - Finds previous object with nested element/group/page + Find previous object with nested element/group/page. :param current_obj: frame index to start :return: None or object with driver_wrapper diff --git a/mops/utils/selector_synchronizer.py b/mops/utils/selector_synchronizer.py index c301b231..e5be1adc 100644 --- a/mops/utils/selector_synchronizer.py +++ b/mops/utils/selector_synchronizer.py @@ -1,25 +1,26 @@ from __future__ import annotations import re -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union from selenium.webdriver.common.by import By from mops.exceptions import InvalidLocatorException -from mops.mixins.objects.locator import Locator from mops.mixins.objects.locator_type import LocatorType from mops.utils.internal_utils import all_tags +if TYPE_CHECKING: + from mops.mixins.objects.locator import Locator -DEFAULT_MATCH = (f"{LocatorType.XPATH}=", f"{LocatorType.ID}=", f"{LocatorType.CSS}=", f"{LocatorType.TEXT}=") -XPATH_MATCH = ("/", "./", "(/") -CSS_MATCH = ("#", ".") -CSS_REGEXP = r"[#.\[\]=]" +DEFAULT_MATCH = (f'{LocatorType.XPATH}=', f'{LocatorType.ID}=', f'{LocatorType.CSS}=', f'{LocatorType.TEXT}=') +XPATH_MATCH = ('/', './', '(/') +CSS_MATCH = ('#', '.') +CSS_REGEXP = r'[#.\[\]=]' -def get_platform_locator(obj: Any): +def get_platform_locator(obj: Any) -> str: """ - Get locator for current platform from object + Get locator for current platform from object. :param obj: Page/Group/Element :return: current platform locator @@ -43,35 +44,34 @@ def get_platform_locator(obj: Any): locator = mobile_fallback_locator if not isinstance(locator, str): - raise InvalidLocatorException(f'Cannot extract locator for current platform for following object: {obj}') + msg = f'Cannot extract locator for current platform for following object: {obj}' + raise InvalidLocatorException(msg) return locator -def set_selenium_selector(obj: Any): - """ - Sets selenium locator & locator type - """ +def set_selenium_selector(obj: Any) -> None: + """Set selenium locator & locator type.""" locator = obj.locator.strip() obj.log_locator = locator # Checking the supported locators - if locator.startswith(f"{LocatorType.XPATH}="): - obj.locator = obj.locator.split(f"{LocatorType.XPATH}=")[-1] + if locator.startswith(f'{LocatorType.XPATH}='): + obj.locator = obj.locator.split(f'{LocatorType.XPATH}=')[-1] obj.locator_type = By.XPATH - elif locator.startswith(f"{LocatorType.TEXT}="): - locator = obj.locator.split(f"{LocatorType.TEXT}=")[-1] + elif locator.startswith(f'{LocatorType.TEXT}='): + locator = obj.locator.split(f'{LocatorType.TEXT}=')[-1] obj.locator = f'//*[contains(text(), "{locator}")]' obj.locator_type = By.XPATH - elif locator.startswith(f"{LocatorType.CSS}="): - obj.locator = obj.locator.split(f"{LocatorType.CSS}=")[-1] + elif locator.startswith(f'{LocatorType.CSS}='): + obj.locator = obj.locator.split(f'{LocatorType.CSS}=')[-1] obj.locator_type = By.CSS_SELECTOR - elif locator.startswith(f"{LocatorType.ID}="): - locator = obj.locator.split(f"{LocatorType.ID}=")[-1] + elif locator.startswith(f'{LocatorType.ID}='): + locator = obj.locator.split(f'{LocatorType.ID}=')[-1] obj.locator = f'[{LocatorType.ID}="{locator}"]' obj.locator_type = By.CSS_SELECTOR @@ -89,7 +89,7 @@ def set_selenium_selector(obj: Any): obj.locator_type = By.CSS_SELECTOR obj.log_locator = f'{LocatorType.CSS}={locator}' - elif " " in locator: + elif ' ' in locator: obj.locator = f'//*[contains(text(), "{locator}")]' obj.locator_type = By.XPATH obj.log_locator = f'{LocatorType.XPATH}={obj.locator}' @@ -97,16 +97,14 @@ def set_selenium_selector(obj: Any): # Default to ID if nothing else matches else: - locator = obj.locator.split(f"{LocatorType.ID}=")[-1] + locator = obj.locator.split(f'{LocatorType.ID}=')[-1] obj.locator = f'[{LocatorType.ID}="{locator}"]' obj.locator_type = By.CSS_SELECTOR obj.log_locator = f'{LocatorType.ID}={locator}' -def set_playwright_locator(obj: Any): - """ - Sets playwright locator & locator type - """ +def set_playwright_locator(obj: Any) -> None: + """Set playwright locator & locator type.""" locator = obj.locator.strip() obj.log_locator = locator @@ -118,7 +116,7 @@ def set_playwright_locator(obj: Any): # Checking the regular locators - elif locator.startswith(XPATH_MATCH): + if locator.startswith(XPATH_MATCH): obj.locator_type = LocatorType.XPATH elif locator.startswith(CSS_MATCH) or re.search(CSS_REGEXP, locator): @@ -127,7 +125,7 @@ def set_playwright_locator(obj: Any): elif locator in all_tags or all(tag in all_tags for tag in locator.split()): obj.locator_type = LocatorType.CSS - elif " " in locator: + elif ' ' in locator: obj.locator_type = LocatorType.TEXT # Default to ID if nothing else matches @@ -139,10 +137,8 @@ def set_playwright_locator(obj: Any): obj.log_locator = obj.locator -def set_appium_selector(obj: Any): - """ - Sets appium locator & locator type - """ +def set_appium_selector(obj: Any) -> None: + """Set appium locator & locator type.""" set_selenium_selector(obj) locator = obj.locator.strip() diff --git a/mops/visual_comparison.py b/mops/visual_comparison.py index 17c16ee5..a579de37 100644 --- a/mops/visual_comparison.py +++ b/mops/visual_comparison.py @@ -1,37 +1,37 @@ from __future__ import annotations +import base64 +from dataclasses import astuple +import importlib +import json +import math import os import re import shutil +from string import punctuation import time -import math -import json -import base64 -import importlib -from dataclasses import astuple +from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union from urllib.parse import urljoin -from typing import Union, List, Any, Tuple, Optional, TYPE_CHECKING -from string import punctuation try: - import cv2.cv2 as cv2 # ~cv2@4.5.5.62 + python@3.8/9/10 + from cv2 import cv2 # ~cv2@4.5.5.62 + python@3.8/9/10 except ImportError: import cv2 # ~cv2@4.10.0.84 + python@3.11/12 -import numpy -from skimage._shared.utils import check_shape_equality # noqa -from skimage.metrics import structural_similarity +import numpy as np from PIL import Image +from skimage._shared.utils import check_shape_equality +from skimage.metrics import structural_similarity -from mops.mixins.objects.size import Size -from mops.exceptions import DriverWrapperException, TimeoutException +from mops.exceptions import DriverWrapperException, TimeoutException, VisualComparisonException from mops.js_scripts import add_element_over_js, delete_element_over_js -from mops.mixins.objects.box import Box -from mops.utils.logs import autolog from mops.mixins.internal_mixin import get_element_info +from mops.mixins.objects.size import Size +from mops.utils.logs import autolog if TYPE_CHECKING: from mops.base.driver_wrapper import DriverWrapper from mops.base.element import Element + from mops.mixins.objects.box import Box class VisualComparison: @@ -84,17 +84,21 @@ def __init__(self, driver_wrapper: DriverWrapper, element: Element = None): self.screenshot_name = 'default' if self.dynamic_threshold_factor and self.default_threshold: - raise Exception('Provide only one argument for threshold of visual comparison') + msg = 'Provide only one argument for threshold of visual comparison' + raise VisualComparisonException(msg) if not self.__initialized: self.__init_session() - def __init_session(self): + def __init_session(self) -> None: root_path = self.visual_regression_path if not root_path: - raise Exception('Provide visual regression path to environment. ' - f'Example: {self.__class__.__name__}.visual_regression_path = "src"') + msg = ( + 'Provide visual regression path to environment. ' + f'Example: {self.__class__.__name__}.visual_regression_path = "src"' + ) + raise VisualComparisonException(msg) root_path = root_path if root_path.endswith('/') else f'{root_path}/' self.reference_directory = f'{root_path}reference/' @@ -112,19 +116,20 @@ def assert_screenshot( filename: str, test_name: str, name_suffix: str, - threshold: Union[int, float], - delay: Union[int, float], + threshold: float, + delay: float, scroll: bool, remove: List[Any], fill_background: Union[str, bool], - cut_box: Optional[Box] + cut_box: Optional[Box], ) -> VisualComparison: """ Assert that the given (by name) and taken screenshots are equal. :param filename: The full screenshot name. A custom filename will be used if an empty string is given. :type filename: str - :param test_name: Test name for the custom filename. It will try to find it automatically if an empty string is given. + :param test_name: Test name for the custom filename. It will try to find it automatically + if an empty string is given. :type test_name: str :param name_suffix: Filename suffix. Useful for the same element with positive/negative cases. :type name_suffix: str @@ -146,7 +151,7 @@ def assert_screenshot( return self remove = remove if remove else [] - screenshot_params = dict(delay=delay, remove=remove, fill_background=fill_background, cut_box=cut_box) + screenshot_params = {'delay': delay, 'remove': remove, 'fill_background': fill_background, 'cut_box': cut_box} if filename: if name_suffix: @@ -167,7 +172,7 @@ def assert_screenshot( return self image = cv2.imread(reference_file) - if isinstance(image, type(None)): + if image is None: self._save_screenshot(reference_file, **screenshot_params) if self.visual_reference_generation or self.soft_visual_reference_generation: @@ -176,8 +181,11 @@ def assert_screenshot( self._disable_reruns() self._attach_allure_diff(reference_file, reference_file, reference_file) - raise AssertionError(f'Reference file "{reference_file}" not found, but its just saved. ' - f'If it CI run, then you need to commit reference files.') from None + msg = ( + f'Reference file "{reference_file}" not found, but its just saved. ' + f'If it CI run, then you need to commit reference files.' + ) + raise AssertionError(msg) from None if self.visual_reference_generation and not self.soft_visual_reference_generation: return self @@ -189,20 +197,20 @@ def assert_screenshot( for file_path in (output_file, diff_file): if os.path.exists(file_path): os.remove(file_path) - except AssertionError as exc: + except AssertionError: if self.soft_visual_reference_generation: if os.path.exists(reference_file): os.remove(reference_file) shutil.move(output_file, reference_file) else: - raise exc + raise return self @staticmethod - def calculate_threshold(file: str, dynamic_threshold_factor: int = None) -> Tuple: + def calculate_threshold(file: str, dynamic_threshold_factor: Optional[int] = None) -> Tuple: """ - Calculate possible threshold, based on dynamic_threshold_factor + Calculate possible threshold, based on dynamic_threshold_factor. :param file: image file path for calculation :param dynamic_threshold_factor: use provided threshold factor @@ -220,11 +228,11 @@ def calculate_threshold(file: str, dynamic_threshold_factor: int = None) -> Tupl def _save_screenshot( self, screenshot_name: str, - delay: Union[int, float], + delay: float, remove: list, fill_background: bool, cut_box: Optional[Box], - ): + ) -> None: time.sleep(delay) self._fill_background(fill_background) @@ -246,7 +254,7 @@ def _save_screenshot( def _appends_dummy_elements(self, remove_data: list) -> VisualComparison: """ - Placed an element above each from given list and paints it black + Placed an element above each from given list and paints it black. :param remove_data: list of elements to be fake removed :return: VisualComparison @@ -264,7 +272,7 @@ def _appends_dummy_elements(self, remove_data: list) -> VisualComparison: def _remove_dummy_elements(self) -> VisualComparison: """ - Remove all dummy elements from DOM + Remove all dummy elements from DOM. :return: VisualComparison """ @@ -273,7 +281,7 @@ def _remove_dummy_elements(self) -> VisualComparison: def _fill_background(self, fill_background_data: Union[bool, str]) -> VisualComparison: """ - Fill background of element + Fill background of element. :param fill_background_data: fill background with given color or black color by default :return: VisualComparison @@ -291,9 +299,9 @@ def _fill_background(self, fill_background_data: Union[bool, str]) -> VisualComp return self def _assert_same_images(self, actual_file: str, reference_file: str, diff_file: str, - threshold: Union[int, float]) -> VisualComparison: + threshold: float) -> VisualComparison: """ - Assert that given images are equal to each other + Assert that given images are equal to each other. :param actual_file: actual image path :param reference_file: reference image path @@ -318,9 +326,12 @@ def _assert_same_images(self, actual_file: str, reference_file: str, diff_file: height, width, _ = reference_image.shape scaled_image = cv2.resize(output_image, (width, height)) cv2.imwrite(diff_file, scaled_image) - raise AssertionError(f"↓\nImage size (width, height) is not same for '{self.screenshot_name}':" - f"\nExpected: {reference_image.shape[0:2]};" - f"\nActual: {output_image.shape[0:2]}.") from None + msg = ( + f"↓\nImage size (width, height) is not same for '{self.screenshot_name}':" + f"\nExpected: {reference_image.shape[0:2]};" + f"\nActual: {output_image.shape[0:2]}." + ) + raise AssertionError(msg) from None diff, actual_threshold = self._get_difference(reference_image, output_image, threshold) is_different = actual_threshold > threshold @@ -329,23 +340,25 @@ def _assert_same_images(self, actual_file: str, reference_file: str, diff_file: cv2.imwrite(diff_file, diff) self._attach_allure_diff(actual_file, reference_file, diff_file) - diff_data = "" + diff_data = '' if self.attach_diff_image_path: diff_data = f"\nDiff image {urljoin('file:', diff_file)}" base_error = f"↓\nVisual mismatch found for '{self.screenshot_name}'{diff_data}" if is_different: - raise AssertionError(f"{base_error}:" - f"\nThreshold is: {actual_threshold};" - f"\nPossible threshold is: {threshold}" - + additional_data) from None + raise AssertionError( + f'{base_error}:' + f'\nThreshold is: {actual_threshold};' + f'\nPossible threshold is: {threshold}' + + additional_data, + ) from None return self def _get_screenshot_name(self, test_function_name: str = '', name_suffix: str = '') -> str: """ - Get screenshot name + Get screenshot name. :param test_function_name: execution test name. Will try to find it automatically if empty string given :return: custom screenshot filename: @@ -358,7 +371,8 @@ def _get_screenshot_name(self, test_function_name: str = '', name_suffix: str = """ test_function_name = test_function_name if test_function_name else getattr(self.test_item, 'name', '') if not test_function_name: - raise Exception('Draft: provide test item self.test_item') + msg = 'Provide VisualComparison.test_item` variable' + raise VisualComparisonException(msg) test_function_name = test_function_name.replace('[', '_') # required here for better separation @@ -374,12 +388,13 @@ def _get_screenshot_name(self, test_function_name: str = '', name_suffix: str = platform_version = caps['platformVersion'] screenshot_name = f'{device_name}_v_{platform_version}_appium_{self.driver_wrapper.browser_name}' elif self.driver_wrapper.is_selenium: - platform_name = self.driver_wrapper.driver.caps["platformName"] + platform_name = self.driver_wrapper.driver.caps['platformName'] screenshot_name = f'{platform_name}_selenium_{self.driver_wrapper.browser_name}' elif self.driver_wrapper.is_playwright: screenshot_name = f'playwright_{self.driver_wrapper.browser_name}' else: - raise DriverWrapperException('Cant find current platform') + msg = 'Cant find current platform' + raise DriverWrapperException(msg) name_suffix = f'_{name_suffix}_' if name_suffix else '_' location_name = self.element_wrapper.name if self.element_wrapper else 'entire_screen' @@ -398,12 +413,12 @@ def _get_screenshot_name(self, test_function_name: str = '', name_suffix: str = def _get_difference( self, - reference_img: numpy.ndarray, - actual_img: numpy.ndarray, - possible_threshold: Union[int, float] - ) -> tuple[numpy.ndarray, float]: + reference_img: np.ndarray, + actual_img: np.ndarray, + possible_threshold: float, + ) -> tuple[np.ndarray, float]: """ - Calculate difference between two images + Calculate difference between two images. :param reference_img: image 1, numpy.ndarray :param actual_img: image 2, numpy.ndarray @@ -421,7 +436,7 @@ def _get_difference( # and is represented as a floating point data type in the range [0,1] # so we must convert the array to 8-bit unsigned integers in the range # [0,255] before we can use it with OpenCV - diff = (diff * 255).astype("uint8") + diff = (diff * 255).astype('uint8') diff_box = cv2.merge([diff, diff, diff]) # Threshold the difference image, followed by finding contours to @@ -430,7 +445,7 @@ def _get_difference( contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = contours[0] if len(contours) == 2 else contours[1] - mask = numpy.zeros(reference_img.shape, dtype='uint8') + mask = np.zeros(reference_img.shape, dtype='uint8') filled_after = actual_img.copy() percent_diff = 100 - score is_different_enough = percent_diff > possible_threshold @@ -447,10 +462,10 @@ def _get_difference( diff_image, percent_diff = filled_after, 100 - score return diff_image, percent_diff - def _attach_allure_diff(self, actual_path: str, expected_path: str, diff_path: str = None) -> None: + def _attach_allure_diff(self, actual_path: str, expected_path: str, diff_path: Optional[str] = None) -> None: """ Attach screenshots to allure screen diff plugin - https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md + https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md. :param actual_path: path of actual image :param expected_path: path of expected image @@ -478,27 +493,27 @@ def _attach_allure_diff(self, actual_path: str, expected_path: str, diff_path: s allure.attach( name=f'diff_for_{self.screenshot_name}', body=json.dumps(diff_dict), - attachment_type='application/vnd.allure.image.diff' + attachment_type='application/vnd.allure.image.diff', ) def _disable_reruns(self) -> None: """ - Disable reruns for pytest + Disable reruns for pytest. :return: None """ try: pytest_rerun = importlib.import_module('pytest_rerunfailures') except ModuleNotFoundError: - return None + return if hasattr(self.test_item, 'execution_count'): self.test_item.execution_count = pytest_rerun.get_reruns_count(self.test_item) + 1 @staticmethod - def _remove_unexpected_underscores(text) -> str: + def _remove_unexpected_underscores(text: str) -> str: """ - Remove multiple underscores from given text + Remove multiple underscores from given text. :return: test_screenshot__data___name -> test_screenshot_data_name """ diff --git a/pyproject.toml b/pyproject.toml index a435320b..ae9009d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ dev-dependencies = [ "furo==2024.8.6", "sphinx==7.1.2", "myst-parser==3.0.1", + "ruff==0.11.2", ] [project.urls] @@ -81,3 +82,50 @@ version = { attr = "mops.__version__" } # Temporary workaround for https://github.com/astral-sh/uv/issues/9513, https://github.com/pypa/setuptools/issues/4759 [tool.setuptools] license-files = [] + +[tool.ruff] +line-length = 120 +include = ["**/mops/**"] +target-version = "py37" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "D415", # First line should end with a period, question mark, or exclamation point + "D212", # Multi-line docstring summary should start at the first line + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "D400", # First line should end with a period + "D100", + "D105", # Missing docstring in magic method + "ANN204", # Missing return type annotation for special method + "INP001", + "UP006", # Use `list` instead of `List` for type annotation + "D203", + "D205", # 1 blank line required between summary line and description + "D213", + "B904", # raise exceptions with `raise ... from err` or `raise ... from None` + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean default positional argument in function definition + "SIM114", # Boolean default positional argument in function definition + "TD", # flake8-todos (TD) + "FIX", # flake8-fixme (FIX) + "PTH", # flake8-use-pathlib (PTH) +] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.isort] +known-first-party = ["mops"] +combine-as-imports = true +force-sort-within-sections = true + +[tool.ruff.lint.pylint] +max-args = 15 + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/tests/adata/pages/PopupsPage.py b/tests/adata/pages/PopupsPage.py new file mode 100644 index 00000000..5d9064c5 --- /dev/null +++ b/tests/adata/pages/PopupsPage.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from mops.base.element import Element +from mops.base.group import Group +from mops.base.page import Page +from tests.settings import domain_name, automation_playground_repo_name + + +class PopupsPage(Page): + + def __init__(self): + super().__init__('popups-page', name='Popups page') + + url = f'{domain_name}/{automation_playground_repo_name}/multi_window.html' + + open_popup_button = Element('#open-popup-button', name='open popup button') + + def open_popup(self) -> OverlayPopup: + self.open_popup_button.scroll_into_view(sleep=0.5).click() + return OverlayPopup().wait_visibility() + +class OverlayPopup(Group): + def __init__(self): + super().__init__('overlay-popup', name='overlay-popup') + + popup_text = Element('popup-text', name='popup text') diff --git a/tests/adata/pages/colored_blocks_page.py b/tests/adata/pages/colored_blocks_page.py index 3778798f..e78dca5d 100644 --- a/tests/adata/pages/colored_blocks_page.py +++ b/tests/adata/pages/colored_blocks_page.py @@ -26,4 +26,4 @@ def __init__(self): navbar = Element('.navbar', name='navbar') def get_all_cards(self) -> List[Element]: - return self.card.wait_elements_count(16).all_elements \ No newline at end of file + return self.card.wait_elements_count(16).all_elements diff --git a/tests/adata/pages/expected_condition_page.py b/tests/adata/pages/expected_condition_page.py index 09d59843..21ab9a77 100644 --- a/tests/adata/pages/expected_condition_page.py +++ b/tests/adata/pages/expected_condition_page.py @@ -9,7 +9,9 @@ class ExpectedConditionPage(Page): def __init__(self): self.url = f'{domain_name}/{automation_playground_repo_name}/expected_conditions.html' self.value_card = WaitValueCard() - self.element_card = WaitElementCard() + self.wait_visibility_card = WaitVisibilityCard() + self.wait_hidden_card = WaitHiddenCard() + self.blinking_card = BlinkingCard() self.frame_card = WaitFrameCard() self.test_driver = self.driver_wrapper super().__init__('//*[contains(@class, "card") and contains(., "wait")]', name='Expected condition page') @@ -21,6 +23,10 @@ def __init__(self): alert_trigger = Element('alert_trigger', name='alert trigger') prompt_trigger = Element('prompt_trigger', name='prompt trigger') + covered_trigger = Element('covered_trigger', name='covered button trigger') + cover_button = Element('cover', name='cover button') + covered_button = Element('covered_button', name='covered button') + alert_handled_badge = Element('alert_handled_badge', name='alert handled badge') confirm_badge = Element('confirm_ok_badge', name='confirm badge') canceled_badge = Element('confirm_cancelled_badge', name='cancelled badge') @@ -29,8 +35,8 @@ def __init__(self): alert_cancel_button = Element(Locator('', ios='//XCUIElementTypeStaticText[@name="Cancel"]'), name='cancel alert button') def set_min_and_max_wait(self, min_wait=1, max_wait=1): - self.min_wait_input.set_text(min_wait) - self.max_wait_input.set_text(max_wait) + self.min_wait_input.set_text(str(min_wait)) + self.max_wait_input.set_text(str(max_wait)) return self @@ -43,15 +49,38 @@ def __init__(self): trigger_button = Element('text_value_trigger', name='trigger wait button') -class WaitElementCard(Group): +class WaitVisibilityCard(Group): def __init__(self): super().__init__('//*[contains(@class, "card") and contains(., "Wait for element to be visible")]', - name='element card') + name='element visible card') trigger_button = Element('visibility_trigger', name='trigger button') target_button = Element('visibility_target', name='target button') +class WaitHiddenCard(Group): + def __init__(self): + super().__init__('//*[contains(@class, "card") and contains(., "Wait for element to be Invisible")]', + name='element hidden card') + + trigger_button = Element('invisibility_trigger', name='trigger button') + target_spinner = Element('invisibility_target', name='target spinner') + + +class BlinkingCard(Group): + def __init__(self): + super().__init__('//*[contains(@class, "card") and contains(., "Wait for blinking panel")]', + name='blinking card') + + interval_input = Element('interval-input', name='interval input') + interval_button = Element('interval-button', name='interval button') + blinking_panel = Element('blinking-panel', name='blinking panel') + + def set_interval(self): + self.interval_input.set_text('400') + self.interval_button.click() + + class WaitFrameCard(Group): def __init__(self): super().__init__('//*[contains(@class, "card") and contains(., "Wait for frame to be available")]', diff --git a/tests/adata/pages/mouse_event_page.py b/tests/adata/pages/mouse_event_page.py index 088bcef7..7f5e5b26 100644 --- a/tests/adata/pages/mouse_event_page.py +++ b/tests/adata/pages/mouse_event_page.py @@ -1,15 +1,13 @@ from __future__ import annotations +from mops.base.driver_wrapper import DriverWrapper from mops.base.element import Element from mops.base.group import Group from mops.base.page import Page from tests.settings import domain_name, automation_playground_repo_name -class MouseEventPage(Page): - def __init__(self, driver_wrapper=None): - self.url = f'{domain_name}/{automation_playground_repo_name}/mouse_events_v2.html' - super().__init__('//h2[.="Mouse Click Actions"]', name='Mouse events page', driver_wrapper=driver_wrapper) +class MouseEventPageMixin: choose_language_button = Element('button.dropbtn', name='"Choose language" button', wait=True) dropdown = Element('div.dropdown-content', name='dropdown with languages') @@ -24,8 +22,23 @@ def mouse_click_card(self): def drag_n_drop(self): return DragAndDrop() +class MouseEventPageV1(Page, MouseEventPageMixin): + url = f'{domain_name}/{automation_playground_repo_name}/mouse_events.html' -class MouseEventPageWithUnexpectedWait(MouseEventPage): + def __init__(self, driver_wrapper: DriverWrapper = None): + super().__init__('//h2[.="Mouse Click Actions"]', name='Mouse events page V1', driver_wrapper=driver_wrapper) + + jump_button = Element('jump-button', name='jumping button') + + +class MouseEventPageV2(Page, MouseEventPageMixin): + url = f'{domain_name}/{automation_playground_repo_name}/mouse_events_v2.html' + + def __init__(self, driver_wrapper: DriverWrapper = None): + super().__init__('//h2[.="Mouse Click Actions"]', name='Mouse events page V2', driver_wrapper=driver_wrapper) + + +class MouseEventPageWithUnexpectedWait(MouseEventPageV2): dropdown = Element('div.dropdown-content', name='dropdown with languages and wait', wait=True) diff --git a/tests/adata/pages/playground_main_page.py b/tests/adata/pages/playground_main_page.py index fd2e7a58..c6071e1c 100644 --- a/tests/adata/pages/playground_main_page.py +++ b/tests/adata/pages/playground_main_page.py @@ -2,12 +2,10 @@ from typing import List -from selenium.webdriver.common.by import By from mops.base.element import Element from mops.base.group import Group from mops.base.page import Page -from mops.mixins.objects.locator import Locator from tests.adata.pages.expected_condition_page import ExpectedConditionPage from tests.adata.pages.keyboard_page import KeyboardPage from tests.settings import domain_name, automation_playground_repo_name diff --git a/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_chrome.png b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_chrome.png new file mode 100644 index 00000000..00a7a5a0 Binary files /dev/null and b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_chrome.png differ diff --git a/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_firefox.png b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_firefox.png new file mode 100644 index 00000000..778cba84 Binary files /dev/null and b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_linux_selenium_firefox.png differ diff --git a/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_chromium.png b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_chromium.png new file mode 100644 index 00000000..591e9df0 Binary files /dev/null and b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_chromium.png differ diff --git a/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_firefox.png b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_firefox.png new file mode 100644 index 00000000..f739ab7c Binary files /dev/null and b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_firefox.png differ diff --git a/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_webkit.png b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_webkit.png new file mode 100644 index 00000000..25e490a8 Binary files /dev/null and b/tests/adata/visual/reference/test_screenshot_popup_with_remove_overlay_popup_playwright_webkit.png differ diff --git a/tests/conftest.py b/tests/conftest.py index c063780d..6881fc69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,13 +12,14 @@ from mops.visual_comparison import VisualComparison from tests.adata.drivers.driver_entities import DriverEntities from tests.adata.drivers.driver_factory import DriverFactory +from tests.adata.pages.PopupsPage import PopupsPage from tests.adata.pages.colored_blocks_page import ColoredBlocksPage from tests.adata.pages.expected_condition_page import ExpectedConditionPage from tests.adata.pages.forms_page import FormsPage from tests.adata.pages.frames_page import FramesPage from tests.adata.pages.keyboard_page import KeyboardPage from tests.adata.pages.progress_bar_page import ProgressBarPage -from tests.adata.pages.mouse_event_page import MouseEventPage +from tests.adata.pages.mouse_event_page import MouseEventPageV1, MouseEventPageV2 from tests.adata.pages.pizza_order_page import PizzaOrderPage from tests.adata.pages.playground_main_page import PlaygroundMainPage, SecondPlaygroundMainPage from tests.adata.pytest_utils import skip_platform @@ -166,14 +167,24 @@ def colored_blocks_page(driver_wrapper): return ColoredBlocksPage().open_page() +@pytest.fixture +def popups_page(driver_wrapper): + return PopupsPage().open_page() + + @pytest.fixture def pizza_order_page(driver_wrapper): return PizzaOrderPage().open_page() @pytest.fixture -def mouse_event_page(driver_wrapper): - return MouseEventPage().open_page() +def mouse_event_page_v1(driver_wrapper): + return MouseEventPageV1().open_page() + + +@pytest.fixture +def mouse_event_page_v2(driver_wrapper): + return MouseEventPageV2().open_page() @pytest.fixture diff --git a/tests/mobile_tests/test_mobile_driver.py b/tests/mobile_tests/test_mobile_driver.py index 1d9a9d99..c6781eda 100644 --- a/tests/mobile_tests/test_mobile_driver.py +++ b/tests/mobile_tests/test_mobile_driver.py @@ -1,11 +1,11 @@ from mops.base.driver_wrapper import DriverWrapper -from tests.adata.pages.mouse_event_page import MouseEventPage +from tests.adata.pages.mouse_event_page import MouseEventPageV2 from tests.adata.pages.pizza_order_page import PizzaOrderPage def test_second_driver_for_mobile_same_page(driver_wrapper, second_driver_wrapper): - mouse_page_mobile = MouseEventPage(driver_wrapper) # mobile driver - mouse_page_desktop = MouseEventPage(second_driver_wrapper) # desktop driver + mouse_page_mobile = MouseEventPageV2(driver_wrapper) # mobile driver + mouse_page_desktop = MouseEventPageV2(second_driver_wrapper) # desktop driver assert len(DriverWrapper.session.all_sessions) == 2 mouse_page_mobile.open_page() @@ -17,7 +17,7 @@ def test_second_driver_for_mobile_same_page(driver_wrapper, second_driver_wrappe def test_second_driver_for_mobile_different_page(driver_wrapper, second_driver_wrapper): pizza_page = PizzaOrderPage(driver_wrapper) # mobile driver - mouse_page = MouseEventPage(second_driver_wrapper) # desktop driver + mouse_page = MouseEventPageV2(second_driver_wrapper) # desktop driver assert len(DriverWrapper.session.all_sessions) == 2 mouse_page.open_page() diff --git a/tests/static_tests/unit/test_wait_condition.py b/tests/static_tests/unit/test_wait_condition.py index d33dfad9..b4a4432d 100644 --- a/tests/static_tests/unit/test_wait_condition.py +++ b/tests/static_tests/unit/test_wait_condition.py @@ -4,7 +4,8 @@ import pytest from mops.exceptions import TimeoutException -from mops.utils.internal_utils import wait_condition, WAIT_METHODS_DELAY +from mops.utils.internal_utils import WAIT_METHODS_DELAY +from mops.utils.decorators import wait_condition from mops.utils.logs import autolog from mops.mixins.objects.wait_result import Result @@ -125,19 +126,23 @@ def test_wait_condition_silent_unexpected_value(silent): def test_wait_condition_mobile_delay_increasing(): + """ sleep for 0.1, 0.2, 0.4 seconds between iterations """ namespace = MockNamespace('wait some condition', call_count=3, is_mobile=True) start_time = time.time() namespace.wait_something() end_time = time.time() - start_time assert end_time > 0.7 - assert end_time < 0.8 + print(end_time) + assert end_time < 0.75 def test_wait_condition_desktop_default_delay(): - namespace = MockNamespace('wait some condition', call_count=5, is_mobile=False) + """ sleep for 0.1 seconds between iterations """ + namespace = MockNamespace('wait some condition', call_count=6, is_mobile=False) start_time = time.time() namespace.wait_something() end_time = time.time() - start_time - assert end_time < 0.6 - assert end_time > 0.5 \ No newline at end of file + assert end_time > 0.6 + print(end_time) + assert end_time < 0.65 diff --git a/tests/web_tests/test_assert_screenshot.py b/tests/web_tests/test_assert_screenshot.py index 8af86c9a..d0f333c6 100644 --- a/tests/web_tests/test_assert_screenshot.py +++ b/tests/web_tests/test_assert_screenshot.py @@ -53,6 +53,14 @@ def test_screenshot_remove(colored_blocks_page): ) +@pytest.mark.low +def test_screenshot_popup_with_remove(popups_page, driver_wrapper): + overlay_popup = popups_page.open_popup() + overlay_popup.assert_screenshot( + remove=[overlay_popup.popup_text], + ) + + @pytest.fixture def file(request): initial_reruns_count = request.node.session.config.option.reruns diff --git a/tests/web_tests/test_checkbox.py b/tests/web_tests/test_checkbox.py index 82c068b2..779701d6 100644 --- a/tests/web_tests/test_checkbox.py +++ b/tests/web_tests/test_checkbox.py @@ -16,10 +16,6 @@ def test_checkbox_uncheck(forms_page): assert all((after_check_checked, after_uncheck_not_checked)) -def test_checkbox_value(forms_page): - assert forms_page.controls_form.python_checkbox.get_attribute('value') == 'PYTHON' - - # Radiobutton tests @@ -36,7 +32,3 @@ def test_radio_uncheck(forms_page): forms_page.controls_form.protractor_radio.check() after_uncheck_not_checked = not forms_page.controls_form.selenium_radio.is_checked() assert all((after_check_checked, after_uncheck_not_checked)) - - -def test_radio_value(forms_page): - assert forms_page.controls_form.selenium_radio.get_attribute('value') == 'SELENIUM' diff --git a/tests/web_tests/test_click.py b/tests/web_tests/test_click.py index 1e54895a..5aad8941 100644 --- a/tests/web_tests/test_click.py +++ b/tests/web_tests/test_click.py @@ -2,6 +2,9 @@ import pytest +from mops.exceptions import ElementNotInteractableException, ContinuousWaitException +from mops.utils.internal_utils import HALF_WAIT_EL, QUARTER_WAIT_EL + def test_click_and_wait(pizza_order_page, platform): pizza_order_page.submit_button.click() @@ -13,15 +16,58 @@ def test_click_and_wait(pizza_order_page, platform): assert all((after_click_displayed, after_click_outside_not_displayed)) -def test_click_into_center(mouse_event_page): - mouse_event_page.mouse_click_card().click_area.click_into_center() - result_x, result_y = mouse_event_page.mouse_click_card().get_result_coordinates() - expected_x_range, expected_y_range = mouse_event_page.mouse_click_card().get_click_area_middle() +def test_click_into_center(mouse_event_page_v2): + mouse_event_page_v2.mouse_click_card().click_area.click_into_center() + result_x, result_y = mouse_event_page_v2.mouse_click_card().get_result_coordinates() + expected_x_range, expected_y_range = mouse_event_page_v2.mouse_click_card().get_click_area_middle() assert result_x in expected_x_range, f'result_x: {result_x}; expected_x: {expected_x_range}' assert result_y in expected_y_range, f'result_y: {result_y}; expected_y: {expected_y_range}' @pytest.mark.parametrize('coordinates', [(-2, -2), (2, 2), (2, -2), (-2, 2), (2, 0), (0, 2)]) -def test_click_outside(mouse_event_page, coordinates): - mouse_event_page.mouse_click_card().click_area_parent.click_outside(*coordinates) - assert not mouse_event_page.mouse_click_card().is_click_proceeded() +def test_click_outside(mouse_event_page_v2, coordinates): + mouse_event_page_v2.mouse_click_card().click_area_parent.click_outside(*coordinates) + assert not mouse_event_page_v2.mouse_click_card().is_click_proceeded() + + +@pytest.mark.low +@pytest.mark.skip_platform('playwright', reason='selenium only') +def test_click_on_covered_button_initial(expected_condition_page, caplog): + assert expected_condition_page.cover_button.is_displayed() + + try: + expected_condition_page.covered_button.click() + except ElementNotInteractableException: + pass + else: + raise AssertionError('Unexpected behaviour. Case not covered') + + +@pytest.mark.low +def test_click_on_covered_button_positive(expected_condition_page, caplog, platform): + expected_condition_page.set_min_and_max_wait(3, 3) + + expected_condition_page.covered_trigger.click() + expected_condition_page.covered_button.click() + + if platform == 'selenium': + assert caplog.messages.count( + 'Caught "ElementNotInteractableException" while executing "click", retrying...') >= 2 + + assert not expected_condition_page.cover_button.is_displayed() + + +@pytest.mark.low +@pytest.mark.skip_platform('playwright', reason='selenium only') +def test_click_on_covered_button_negative(expected_condition_page, caplog): + expected_condition_page.set_min_and_max_wait(20, 20) + + expected_condition_page.covered_trigger.click() + + start = time.time() + try: # The retry logic should be equal to 5 seconds from first exception + some time for first execution + expected_condition_page.covered_button.click() + except ElementNotInteractableException: + assert time.time() - start < HALF_WAIT_EL + QUARTER_WAIT_EL + else: + raise AssertionError('Unexpected behaviour. Case not covered') diff --git a/tests/web_tests/test_driver.py b/tests/web_tests/test_driver.py index e4a0c2e3..4e70bd92 100644 --- a/tests/web_tests/test_driver.py +++ b/tests/web_tests/test_driver.py @@ -1,7 +1,7 @@ import pytest from mops.base.driver_wrapper import DriverWrapper -from tests.adata.pages.mouse_event_page import MouseEventPage +from tests.adata.pages.mouse_event_page import MouseEventPageV2 from tests.adata.pages.pizza_order_page import PizzaOrderPage from tests.adata.pages.playground_main_page import SecondPlaygroundMainPage from tests.conftest import DESKTOP_WINDOW_SIZE @@ -28,7 +28,7 @@ def test_is_firefox_driver(driver_wrapper): assert not driver_wrapper.is_chrome -def test_driver_cookies(driver_wrapper, mouse_event_page): +def test_driver_cookies(driver_wrapper, mouse_event_page_v2): driver_wrapper.set_cookie( [ {'name': 'sample_cookie', 'value': '123', 'path': '/', 'domain': '.customenv.github.io'} @@ -42,7 +42,7 @@ def test_driver_cookies(driver_wrapper, mouse_event_page): assert all((actual_cookies_after_set, not actual_cookies_after_clear)) -def test_driver_delete_cookie(driver_wrapper, mouse_event_page): +def test_driver_delete_cookie(driver_wrapper, mouse_event_page_v2): cookie_name_1 = 'sample_cookie' cookie_name_2 = 'another_cookie' driver_wrapper.set_cookie( @@ -60,25 +60,25 @@ def test_driver_delete_cookie(driver_wrapper, mouse_event_page): assert actual_cookies_after_clear[0]['name'] == cookie_name_1 -def test_driver_execute_script_set_and_get(driver_wrapper, mouse_event_page): +def test_driver_execute_script_set_and_get(driver_wrapper, mouse_event_page_v2): driver_wrapper.execute_script('sessionStorage.setItem("foo", "bar")') assert driver_wrapper.execute_script('return sessionStorage.getItem("foo")') == 'bar' -def test_driver_execute_script_return_value(driver_wrapper, mouse_event_page): +def test_driver_execute_script_return_value(driver_wrapper, mouse_event_page_v2): assert driver_wrapper.execute_script('return document.title;') == 'Mouse Actions v2' -def test_driver_execute_script_with_args(driver_wrapper, mouse_event_page): +def test_driver_execute_script_with_args(driver_wrapper, mouse_event_page_v2): main_page = SecondPlaygroundMainPage() assert not main_page.is_page_opened() - driver_wrapper.execute_script('arguments[0].click();', mouse_event_page.header_logo) + driver_wrapper.execute_script('arguments[0].click();', mouse_event_page_v2.header_logo) assert main_page.wait_page_loaded().is_page_opened() @pytest.mark.low def test_second_driver_different_page(driver_wrapper, second_driver_wrapper): - mouse_page = MouseEventPage(second_driver_wrapper) + mouse_page = MouseEventPageV2(second_driver_wrapper) pizza_page = PizzaOrderPage(driver_wrapper) assert len(DriverWrapper.session.all_sessions) == 2 @@ -99,8 +99,8 @@ def test_second_driver_different_page(driver_wrapper, second_driver_wrapper): @pytest.mark.low def test_second_driver_same_page(driver_wrapper, second_driver_wrapper): - mouse_page1 = MouseEventPage(driver_wrapper) - mouse_page2 = MouseEventPage(second_driver_wrapper) + mouse_page1 = MouseEventPageV2(driver_wrapper) + mouse_page2 = MouseEventPageV2(second_driver_wrapper) assert len(DriverWrapper.session.all_sessions) == 2 mouse_page1.open_page() @@ -118,7 +118,7 @@ def test_second_driver_same_page(driver_wrapper, second_driver_wrapper): @pytest.mark.low def test_second_driver_by_arg(driver_wrapper, second_driver_wrapper): pizza_page = PizzaOrderPage(driver_wrapper) - mouse_page = MouseEventPage(second_driver_wrapper) + mouse_page = MouseEventPageV2(second_driver_wrapper) assert len(DriverWrapper.session.all_sessions) == 2 mouse_page.open_page() @@ -174,7 +174,7 @@ def test_driver_tabs(driver_wrapper, second_playground_page): @pytest.mark.low def test_parent_in_hidden_element(driver_wrapper, second_driver_wrapper): pizza_page = PizzaOrderPage(driver_wrapper) - mouse_page = MouseEventPage(second_driver_wrapper) + mouse_page = MouseEventPageV2(second_driver_wrapper) card = mouse_page.mouse_click_card() @@ -197,7 +197,7 @@ def test_parent_in_hidden_element(driver_wrapper, second_driver_wrapper): @pytest.mark.low def test_driver_in_hidden_group(driver_wrapper, second_driver_wrapper): pizza_page = PizzaOrderPage(driver_wrapper) - mouse_page = MouseEventPage(second_driver_wrapper) + mouse_page = MouseEventPageV2(second_driver_wrapper) mouse_page.open_page() pizza_page.open_page() @@ -226,7 +226,7 @@ def test_driver_in_hidden_page(driver_wrapper, second_driver_wrapper): @pytest.mark.low def test_second_driver_in_parent_element(driver_wrapper, second_driver_wrapper): - mouse_page2 = MouseEventPage(second_driver_wrapper) + mouse_page2 = MouseEventPageV2(second_driver_wrapper) mouse_page2.open_page() card = mouse_page2.drag_n_drop() diff --git a/tests/web_tests/test_element.py b/tests/web_tests/test_element.py index d7e74a0b..1be5f57a 100644 --- a/tests/web_tests/test_element.py +++ b/tests/web_tests/test_element.py @@ -45,12 +45,12 @@ def test_type_clear_text_get_value(pizza_order_page): @pytest.mark.xfail_platform('selenium-safari', reason='Fail in CI env') -def test_hover(mouse_event_page): - initial_not_displayed = not mouse_event_page.dropdown.is_displayed() - mouse_event_page.choose_language_button.scroll_into_view(sleep=0.1).hover() - after_hover_displayed = mouse_event_page.dropdown.wait_visibility_without_error().is_displayed() - mouse_event_page.choose_language_button.hover_outside() - after_outside_hover_displayed = not mouse_event_page.dropdown.wait_hidden().is_displayed() +def test_hover(mouse_event_page_v2): + initial_not_displayed = not mouse_event_page_v2.dropdown.is_displayed() + mouse_event_page_v2.choose_language_button.scroll_into_view(sleep=0.1).hover() + after_hover_displayed = mouse_event_page_v2.dropdown.wait_visibility_without_error().is_displayed() + mouse_event_page_v2.choose_language_button.hover_outside() + after_outside_hover_displayed = not mouse_event_page_v2.dropdown.wait_hidden().is_displayed() assert all((initial_not_displayed, after_hover_displayed, after_outside_hover_displayed)) @@ -113,7 +113,7 @@ def test_element_execute_script(forms_page, driver_wrapper): forms_page.controls_form.german_slider.execute_script('arguments[0].textContent = arguments[1];', new_text) assert forms_page.controls_form.german_slider.text == new_text -def test_element_locator_check(mouse_event_page, driver_wrapper): +def test_element_locator_check(mouse_event_page_v2, driver_wrapper): # Let's keep Elements here, for encapsulation purposes # Reformat test if any trouble occur locators = ( diff --git a/tests/web_tests/test_element_attribute.py b/tests/web_tests/test_element_attribute.py new file mode 100644 index 00000000..3ca81240 --- /dev/null +++ b/tests/web_tests/test_element_attribute.py @@ -0,0 +1,17 @@ +import pytest + + +def test_element_attribute_radio_button(forms_page): + assert forms_page.controls_form.selenium_radio.get_attribute('value') == 'SELENIUM' + + +def test_element_attribute_checkbox(forms_page): + assert forms_page.controls_form.python_checkbox.get_attribute('value') == 'PYTHON' + + +@pytest.mark.skip_platform('playwright', reason='selenium only case') +def test_get_attribute_dynamic_element(mouse_event_page_v1): + button = mouse_event_page_v1.jump_button + for i in range(20): + text = button.get_attribute('innerText') or button.get_attribute('textContent') + assert text in ('Container 1', 'Container 2'), f'Actual text: {text}' diff --git a/tests/web_tests/test_element_text.py b/tests/web_tests/test_element_text.py new file mode 100644 index 00000000..01fdb8d9 --- /dev/null +++ b/tests/web_tests/test_element_text.py @@ -0,0 +1,41 @@ +import pytest + + +@pytest.mark.low +def test_wait_for_text_dynamic_element(mouse_event_page_v1): + """ Case: 102034539 """ + for i in range(20): + assert mouse_event_page_v1.jump_button.wait_for_text(timeout=1) + +@pytest.mark.low +def test_get_text_dynamic_element(mouse_event_page_v1): + """ Case: 89391107 """ + for i in range(20): + assert mouse_event_page_v1.jump_button.text in ('Container 1', 'Container 2') + +@pytest.mark.xfail_platform('android', 'ios', reason='Can not get value from that element. TODO: Rework test') +def test_wait_element_value(expected_condition_page): + expected_condition_page.value_card.trigger_button.click() + value_without_wait = expected_condition_page.value_card.wait_for_value_input.value + expected_condition_page.value_card.wait_for_value_input.wait_for_value() + value_with_wait = expected_condition_page.value_card.wait_for_value_input.value == 'Dennis Ritchie' + assert all((not value_without_wait, value_with_wait)) + + +def test_wait_element_text(expected_condition_page, driver_wrapper): + btn = expected_condition_page.value_card.wait_for_text_button + + expected_condition_page.value_card.trigger_button.click() + value_without_wait = btn.text + assert not value_without_wait + assert btn.wait_for_text().text == 'Submit' + + +def test_wait_empty_element_text(expected_condition_page, driver_wrapper): + btn = expected_condition_page.value_card.wait_for_text_button + assert btn.wait_for_text('').text == '' + + +def test_wait_empty_element_value(expected_condition_page): + btn = expected_condition_page.value_card.wait_for_value_input + assert btn.wait_for_value('').value == '' diff --git a/tests/web_tests/test_group.py b/tests/web_tests/test_group.py index ddca4b97..5696eb9e 100644 --- a/tests/web_tests/test_group.py +++ b/tests/web_tests/test_group.py @@ -1,23 +1,23 @@ -from tests.adata.pages.mouse_event_page import MouseEventPage +from tests.adata.pages.mouse_event_page import MouseEventPageV2 -def test_group_with_class_var_positive(mouse_event_page): - mouse_click_card = MouseEventPage().mouse_click_card() +def test_group_with_class_var_positive(mouse_event_page_v2): + mouse_click_card = MouseEventPageV2().mouse_click_card() assert mouse_click_card.click_area.is_displayed() -def test_group_with_class_var_negative(mouse_event_page): - mouse_click_card = MouseEventPage().mouse_click_card() +def test_group_with_class_var_negative(mouse_event_page_v2): + mouse_click_card = MouseEventPageV2().mouse_click_card() assert not mouse_click_card.drag_source.is_displayed() -def test_group_with_class_func_positive(mouse_event_page): - mouse_click_card = MouseEventPage().mouse_click_card() +def test_group_with_class_func_positive(mouse_event_page_v2): + mouse_click_card = MouseEventPageV2().mouse_click_card() assert mouse_click_card.click_area_func().is_displayed() -def test_group_with_class_func_negative(mouse_event_page): - mouse_click_card = MouseEventPage().mouse_click_card() +def test_group_with_class_func_negative(mouse_event_page_v2): + mouse_click_card = MouseEventPageV2().mouse_click_card() assert not mouse_click_card.drag_source_func().is_displayed() diff --git a/tests/web_tests/test_page.py b/tests/web_tests/test_page.py index a9a3e864..2cf59402 100644 --- a/tests/web_tests/test_page.py +++ b/tests/web_tests/test_page.py @@ -6,13 +6,13 @@ @pytest.mark.parametrize('with_elements_case', (True, False), ids=['with elements', 'without elements']) -def test_page_loaded_positive(mouse_event_page, with_elements_case): - mouse_event_page.wait_page_loaded() - assert mouse_event_page.is_page_opened(with_elements=with_elements_case) +def test_page_loaded_positive(mouse_event_page_v2, with_elements_case): + mouse_event_page_v2.wait_page_loaded() + assert mouse_event_page_v2.is_page_opened(with_elements=with_elements_case) @pytest.mark.parametrize('with_elements_case', (True, False), ids=['with elements', 'without elements']) -def test_page_loaded_negative(mouse_event_page, with_elements_case): +def test_page_loaded_negative(mouse_event_page_v2, with_elements_case): page = MouseEventPageWithUnexpectedWait() try: page.wait_page_loaded(timeout=0.1) diff --git a/tests/web_tests/test_wait.py b/tests/web_tests/test_wait.py index ac1a2241..483a3e36 100644 --- a/tests/web_tests/test_wait.py +++ b/tests/web_tests/test_wait.py @@ -1,56 +1,13 @@ import pytest +from mops.exceptions import ContinuousWaitException, TimeoutException -# TODO: rework needed -def test_wait_element(pizza_order_page): - pizza_order_page.submit_button.wait_visibility() - assert pizza_order_page.submit_button.is_displayed() - -# TODO: rework needed -def test_wait_without_error(pizza_order_page): - pizza_order_page.error_modal.wait_visibility_without_error(timeout=0.5) - assert not pizza_order_page.error_modal.is_displayed() - - -# TODO: rework needed -def test_wait_hidden(pizza_order_page): - pizza_order_page.error_modal.wait_visibility_without_error(timeout=1) - assert not pizza_order_page.error_modal.is_displayed() - - -# TODO: rework needed -def test_wait_hidden_without_error(pizza_order_page): - pizza_order_page.submit_button.wait_visibility_without_error(timeout=0.5) - assert pizza_order_page.submit_button.is_displayed() - - -@pytest.mark.xfail_platform('android', 'ios', reason='Can not get value from that element. TODO: Rework test') -def test_wait_element_value(expected_condition_page): - expected_condition_page.value_card.trigger_button.click() - value_without_wait = expected_condition_page.value_card.wait_for_value_input.value - expected_condition_page.value_card.wait_for_value_input.wait_for_value() - value_with_wait = expected_condition_page.value_card.wait_for_value_input.value == 'Dennis Ritchie' - assert all((not value_without_wait, value_with_wait)) - - -def test_wait_element_text(expected_condition_page, driver_wrapper): - btn = expected_condition_page.value_card.wait_for_text_button - - expected_condition_page.value_card.trigger_button.click() - value_without_wait = btn.text - assert not value_without_wait - assert btn.wait_for_text().text == 'Submit' - - -def test_wait_empty_element_text(expected_condition_page, driver_wrapper): - btn = expected_condition_page.value_card.wait_for_text_button - assert btn.wait_for_text('').text == '' - - -def test_wait_empty_element_value(expected_condition_page): - btn = expected_condition_page.value_card.wait_for_value_input - assert btn.wait_for_value('').value == '' +def test_get_rect_dynamic_element(mouse_event_page_v1): + for i in range(20): + assert all(mouse_event_page_v1.jump_button.get_rect().values()) + assert mouse_event_page_v1.jump_button.location + assert mouse_event_page_v1.jump_button.size def test_wait_elements_count_v1(forms_page): @@ -81,3 +38,83 @@ def test_wait_element_stop_moving(progressbar_page): # progressbar_page.start_button.click() # locations_list = [tuple(bar.location.values()) for _ in range(200) if not time.sleep(0.1)] pass + + +# Wait cases + + +def test_wait_hidden_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + expected_condition_page.wait_hidden_card.trigger_button.click() + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden() + assert 'Wait until "target spinner" becomes hidden' in str(caplog.messages) + assert expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + + +def test_wait_visibility_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + expected_condition_page.wait_visibility_card.trigger_button.click() + expected_condition_page.wait_visibility_card.target_button.wait_visibility() + assert 'Wait until "target button" becomes visible' in str(caplog.messages) + assert expected_condition_page.wait_visibility_card.target_button.is_displayed() + + +def test_wait_hidden_negative(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + try: + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden(timeout=0.5) + except TimeoutException as exc: + assert ('"target spinner" still visible after 0.5 seconds. Selector=\'xpath=//*[contains(@class, "card") and' + ' contains(., "Wait for element to be Invisible")] >> id=invisibility_target\'.') in exc.msg + else: + raise Exception('Unexpected behaviour. Case not covered') + + +def test_wait_visibility_negative(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + try: + expected_condition_page.wait_visibility_card.target_button.wait_visibility(timeout=0.5) + except TimeoutException as exc: + assert ('"target button" not visible after 0.5 seconds. Selector=\'xpath=//*[contains(@class, "card") and' + ' contains(., "Wait for element to be visible")] >> id=visibility_target\'.') in exc.msg + else: + raise Exception('Unexpected behaviour. Case not covered') + + +# Continuous wait cases + + +def test_wait_continuous_hidden_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + expected_condition_page.wait_hidden_card.trigger_button.click() + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden(continuous=1) + assert 'Starting continuous "wait_hidden" for the "target spinner" for next 1 seconds' in str(caplog.messages) + assert expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + + +def test_wait_continuous_visibility_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + expected_condition_page.wait_visibility_card.trigger_button.click() + expected_condition_page.wait_visibility_card.target_button.wait_visibility(continuous=1) + assert 'Starting continuous "wait_visibility" for the "target button" for next 1 seconds' in str(caplog.messages) + assert expected_condition_page.wait_visibility_card.target_button.is_displayed() + + +def test_wait_continuous_hidden_negative(expected_condition_page, caplog): + expected_condition_page.blinking_card.set_interval() + try: + expected_condition_page.blinking_card.blinking_panel.wait_hidden(continuous=True) + except ContinuousWaitException as exc: + assert 'The continuous "wait_hidden" of the "blinking panel" is no met after 0.' in exc.msg + else: + raise Exception('Unexpected behaviour. Case not covered') + + +def test_wait_continuous_visibility_negative(expected_condition_page, caplog): + expected_condition_page.blinking_card.set_interval() + try: + expected_condition_page.blinking_card.blinking_panel.wait_visibility(continuous=True) + except ContinuousWaitException as exc: + assert 'The continuous "wait_visibility" of the "blinking panel" is no met after 0.' in exc.msg + else: + raise Exception('Unexpected behaviour. Case not covered') diff --git a/tests/web_tests/test_wait_wihtout_error.py b/tests/web_tests/test_wait_wihtout_error.py new file mode 100644 index 00000000..a125316b --- /dev/null +++ b/tests/web_tests/test_wait_wihtout_error.py @@ -0,0 +1,64 @@ +# Wait cases + + +def test_wait_hidden_without_error_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + expected_condition_page.wait_hidden_card.trigger_button.click() + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden_without_error() + assert expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + + +def test_wait_visibility_without_error_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + expected_condition_page.wait_visibility_card.trigger_button.click() + expected_condition_page.wait_visibility_card.target_button.wait_visibility_without_error() + assert expected_condition_page.wait_visibility_card.target_button.is_displayed() + + +def test_wait_hidden_without_error_negative(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + expected_condition_page.wait_hidden_card.trigger_button.click() + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden_without_error(timeout=0.5) + assert expected_condition_page.wait_hidden_card.target_spinner.is_displayed() + assert 'Ignored exception:' in str(caplog.messages) + + +def test_wait_visibility_without_error_negative(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + expected_condition_page.wait_visibility_card.trigger_button.click() + expected_condition_page.wait_visibility_card.target_button.wait_visibility_without_error(timeout=0.5) + assert expected_condition_page.wait_visibility_card.target_button.is_hidden() + assert 'Ignored exception:' in str(caplog.messages) + + +# Continuous wait cases + + +def test_wait_continuous_hidden_without_error_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + expected_condition_page.wait_hidden_card.trigger_button.click() + expected_condition_page.wait_hidden_card.target_spinner.wait_hidden_without_error(continuous=1) + assert 'Wait until "target spinner" becomes continuous hidden without error exception' in str(caplog.messages) + assert expected_condition_page.wait_hidden_card.target_spinner.is_hidden() + + +def test_wait_continuous_visibility_without_error_positive(expected_condition_page, caplog): + assert not expected_condition_page.wait_visibility_card.target_button.is_displayed() + expected_condition_page.wait_visibility_card.trigger_button.click() + expected_condition_page.wait_visibility_card.target_button.wait_visibility_without_error(continuous=1) + assert 'Wait until "target button" becomes continuous visible without error exception' in str(caplog.messages) + assert expected_condition_page.wait_visibility_card.target_button.is_displayed() + + +def test_wait_continuous_hidden_without_error_negative(expected_condition_page, caplog): + expected_condition_page.blinking_card.set_interval() + expected_condition_page.blinking_card.blinking_panel.wait_hidden_without_error(continuous=True) + assert 'The continuous "wait_hidden" of the "blinking panel" is no met after 0.' in str(caplog.messages) + assert expected_condition_page.blinking_card.blinking_panel.is_displayed() + + +def test_wait_continuous_visibility_without_error_negative(expected_condition_page, caplog): + expected_condition_page.blinking_card.set_interval() + expected_condition_page.blinking_card.blinking_panel.wait_visibility_without_error(continuous=True) + assert 'The continuous "wait_visibility" of the "blinking panel" is no met after 0.' in str(caplog.messages) + assert expected_condition_page.blinking_card.blinking_panel.is_hidden() diff --git a/uv.lock b/uv.lock index 1f55bece..912cb77d 100644 --- a/uv.lock +++ b/uv.lock @@ -435,15 +435,36 @@ wheels = [ name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')", +] dependencies = [ { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, ] +[[package]] +name = "importlib-metadata" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version == '3.9.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.9.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.9.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.9.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "zipp", version = "3.21.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -667,7 +688,7 @@ wheels = [ [[package]] name = "mops" -version = "3.1.0" +version = "3.2.0" source = { virtual = "." } dependencies = [ { name = "appium-python-client" }, @@ -696,6 +717,7 @@ dev = [ { name = "playwright" }, { name = "pytest" }, { name = "pytest-rerunfailures" }, + { name = "ruff" }, { name = "selenium" }, { name = "sphinx" }, ] @@ -725,6 +747,7 @@ dev = [ { name = "playwright", specifier = "==1.48.0" }, { name = "pytest", specifier = "==7.4.3" }, { name = "pytest-rerunfailures", specifier = "==11.1" }, + { name = "ruff", specifier = "==0.11.2" }, { name = "selenium", specifier = "==4.12.0" }, { name = "sphinx", specifier = "==7.1.2" }, ] @@ -1301,11 +1324,11 @@ wheels = [ [[package]] name = "pytz" -version = "2024.2" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] [[package]] @@ -1411,14 +1434,38 @@ dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, - { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "urllib3", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +] + [[package]] name = "scikit-image" version = "0.21.0" @@ -1738,8 +1785,7 @@ dependencies = [ { name = "trio", version = "0.27.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "trio", version = "0.28.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "trio-websocket" }, - { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, extra = ["socks"], marker = "python_full_version < '3.9'" }, - { name = "urllib3", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, extra = ["socks"], marker = "python_full_version >= '3.9'" }, + { name = "urllib3", extra = ["socks"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/12/909a37b8ab8a278efbd6566d2783dda657be07b84d20e3bfc09b18ec715e/selenium-4.12.0.tar.gz", hash = "sha256:95be6aa449a0ab4ac1198bb9de71bbe9170405e04b9752f4b450dc7292a21828", size = 9141240 } wheels = [ @@ -1802,7 +1848,8 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, @@ -2211,11 +2258,6 @@ wheels = [ name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.9' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.9' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.9' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.9' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.9' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, @@ -2223,35 +2265,7 @@ wheels = [ [package.optional-dependencies] socks = [ - { name = "pysocks", marker = "python_full_version < '3.9'" }, -] - -[[package]] -name = "urllib3" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.9.*' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version == '3.9.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.9.*' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version == '3.9.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.9.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, -] - -[package.optional-dependencies] -socks = [ - { name = "pysocks", marker = "python_full_version >= '3.9'" }, + { name = "pysocks" }, ] [[package]]