from __future__ import annotations
from abc import ABCMeta
from copy import copy
import functools
from functools import cached_property
import time
from typing import TYPE_CHECKING, Any
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.exceptions import (
ContinuousWaitException,
DriverWrapperException,
TimeoutException,
UnexpectedElementsCountException,
UnexpectedElementSizeException,
UnexpectedTextException,
UnexpectedValueException,
UnsuitableArgumentsException,
)
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.scrolls import ScrollTo, ScrollTypes, scroll_into_view_blocks
from mops.mixins.objects.visual_comaprison_mixin import hide_before_screenshot, reveal_after_screenshot
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.utils.decorators import wait_condition, wait_continuous
from mops.utils.internal_utils import (
QUARTER_WAIT_EL,
WAIT_EL,
extract_named_objects,
initialize_objects,
is_target_on_screen,
set_parent_for_attr,
)
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 typing import Self
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
class ElementMeta(ABCMeta):
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) -> ElementMeta:
"""Create a new Element class and wrap its __init__ to call _modify_sub_elements."""
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
orig_init = cls.__init__
@functools.wraps(orig_init)
def wrapped_init(self: Any, *args: Any, **kw: Any) -> None:
orig_init(self, *args, **kw)
if type(self) is cls and getattr(self, '_initialized', False):
self._modify_sub_elements()
cls.__init__ = wrapped_init
return cls
[docs]class Element(DriverMixin, InternalMixin, Logging, ElementABC, metaclass=ElementMeta):
"""
Represents a UI element that serves as a central component for interaction.
The :class:`Element` class is designed to be used within :class:`.Page` or :class:`.Group` objects.
It dynamically adapts to different driver types (Playwright, Appium, Selenium)
and provides a unified interface for UI interactions.
"""
_object: str = 'element'
_initialized: bool = False
_is_locator_configured: bool = False
_base_cls: type[PlayElement, MobileElement, WebElement]
source_locator: Locator | str
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
"""Create a new Element instance and set the frame for multi-session tracking."""
instance = super().__new__(cls)
set_instance_frame(instance)
return instance
def __copy__(self) -> Self:
"""Return a shallow copy of this Element."""
new = object.__new__(self.__class__)
new.__dict__.update(self.__dict__)
return new
def __repr__(self) -> str:
"""Return a string representation of this Element."""
return self._repr_builder()
def __call__(self, driver_wrapper: DriverWrapper = None) -> Self:
"""Initialize the element with the given driver wrapper and return self."""
self.__full_init__(driver_wrapper=get_driver_wrapper_from_object(driver_wrapper))
return self
[docs] def __init__(
self,
locator: Locator | str,
name: str = '',
parent: Group | Element | bool = None,
wait: bool | None = None,
driver_wrapper: DriverWrapper | Any = None,
):
"""
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.
:param locator: The element's locator. `.LocatorType` is optional.
:type locator: typing.Union[Locator, str]
:param name: The name of the element, used for logging and identification purposes.
:type name: str
:param parent: The parent of the element. Provide :obj:`False` to skip association.
:type parent: typing.Union[Group, Element, bool]
:param wait: If `True`, the element will be checked in
`wait_page_loaded` and `is_page_opened` methods of `Page`.
:type wait: typing.Optional[bool]
:param driver_wrapper: The :class:`.DriverWrapper` instance or
an object containing it to be used for this element.
:type driver_wrapper: typing.Union[DriverWrapper, typing.Any]
"""
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)
self.source_locator = locator
self.locator = locator
self.name = name or locator
self.parent = parent
self.wait = wait
self._safe_setter('__base_obj_id', id(self))
if self.driver_wrapper:
self.__full_init__(driver_wrapper)
def __full_init__(self, driver_wrapper: Any = None):
self._driver_wrapper_given = bool(driver_wrapper)
if driver_wrapper and driver_wrapper != self.driver_wrapper:
self.driver_wrapper = get_driver_wrapper_from_object(driver_wrapper)
self._modify_object()
if not self._initialized:
self.__init_base_class__()
def __init_base_class__(self) -> None:
"""
Initialise base class according to current driver, and set his methods
:return: None
"""
if self._driver_is_instance(PlaywrightDriver):
self._base_cls = PlayElement
elif self._driver_is_instance(AppiumDriver):
self._base_cls = MobileElement
elif self._driver_is_instance(SeleniumDriver):
self._base_cls = WebElement
else:
msg = (
f'Cannot initialize {self.__class__.__name__}: '
f'unsupported driver type "{type(self.driver).__name__}". '
f'Expected Playwright, Appium or Selenium driver instance'
)
raise DriverWrapperException(msg)
self._set_static(self._base_cls)
self._base_cls.__init__(self)
self._initialized = True
@property
def locator(self) -> str:
"""Return the element locator string."""
if self.driver_wrapper and not self._is_locator_configured:
self._set_locator()
return self._locator
@locator.setter
def locator(self, value: Locator | str) -> None:
"""Set the element locator."""
self._log_locator = value
self._locator = value
@property
def locator_type(self) -> str:
"""Return the element locator type."""
if self.driver_wrapper and not self._is_locator_configured:
self._set_locator()
return self._locator_type
@locator_type.setter
def locator_type(self, value: str) -> None:
"""Set the element locator type."""
self._locator_type = value
@property
def log_locator(self) -> str:
"""Return the element locator string for logging."""
if self.driver_wrapper and not self._is_locator_configured:
self._set_locator()
return self._log_locator
@log_locator.setter
def log_locator(self, value: str) -> None:
"""Set the element locator string for logging."""
self._log_locator = value
# Following methods works same for both Selenium/Appium and Playwright APIs using internal methods
# Elements interaction
[docs] def set_text(self, text: str, silent: bool = False) -> Element:
"""
Clear the current input field and type the provided text.
:param text: The text to enter into the element.
:type text: str
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
if not silent:
self.log(f'Set text in "{self.name}"')
self.clear_text(silent=True).type_text(text, silent=True)
return self
[docs] def send_keyboard_action(self, action: str | KeyboardKeys) -> Element:
"""
Send a keyboard action to the current element (e.g., press a key or shortcut).
:param action: The keyboard action to perform.
:type action: str or :class:`KeyboardKeys`
:return: :class:`Element`
"""
if self.driver_wrapper.is_playwright:
self.click()
self.driver.keyboard.press(action)
else:
self.type_text(action)
return self
# Elements waits
[docs] @wait_continuous
@wait_condition
def wait_visibility(
self,
*,
timeout: int = WAIT_EL,
silent: bool = False,
continuous: 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(
execution_result=self.is_displayed(silent=True),
log=f'Wait until "{self.name}" becomes visible',
exc=TimeoutException(f'"{self.name}" not visible', info=self),
)
[docs] def wait_visibility_without_error(
self,
*,
timeout: float = QUARTER_WAIT_EL,
silent: bool = False,
continuous: 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.
A continuous visibility verification may be applied for given
or default amount of time after the first condition is met.
**Selenium & Playwright:**
- 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:`QUARTER_WAIT_EL`.
: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:
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, continuous=continuous)
except (TimeoutException, WebDriverException, ContinuousWaitException) as exception:
if not silent:
self.log(f'Ignored exception: "{exception.msg}"')
return self
[docs] @wait_continuous
@wait_condition
def wait_hidden(
self,
*,
timeout: int = WAIT_EL,
silent: bool = False,
continuous: 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(
execution_result=self.is_hidden(silent=True),
log=f'Wait until "{self.name}" becomes hidden',
exc=TimeoutException(f'"{self.name}" still visible', info=self),
)
[docs] def wait_hidden_without_error(
self,
*,
timeout: float = QUARTER_WAIT_EL,
silent: bool = False,
continuous: 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.
A continuous invisibility verification may be applied for given
or default amount of time after the first condition is met.
**Selenium & Playwright:**
- 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:`QUARTER_WAIT_EL`.
: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:
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=silent, continuous=continuous)
except (TimeoutException, WebDriverException, ContinuousWaitException) as exception:
if not silent:
self.log(f'Ignored exception: "{exception.msg}"')
return self
[docs] @wait_condition
def wait_availability(self, *, timeout: int = WAIT_EL, silent: bool = False) -> Element:
r"""
Wait 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:`Element`
"""
return Result(
execution_result=self.is_available(),
log=f'Wait until presence of "{self.name}"',
exc=TimeoutException(f'"{self.name}" not available in DOM', info=self),
)
[docs] @wait_condition
def wait_for_text(
self,
expected_text: str | None = None,
*,
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.
**Note:** The method requires the use of named arguments except ``expected_text``.
**Selenium & Playwright:**
- 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 expected_text: The text to wait for. :obj:`None` - any text; :class:`str` - expected text.
:type expected_text: typing.Optional[str]
:param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`.
:type timeout: typing.Union[int, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
actual_text = self.text
if expected_text is not None:
result = actual_text == expected_text
error = f'Not expected text for "{self.name}"'
log_msg = f'Wait until text of "{self.name}" will be equal to "{expected_text}"'
else:
result = actual_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))
[docs] @wait_condition
def wait_for_value(
self,
expected_value: str | None = None,
*,
timeout: float = WAIT_EL,
silent: bool = False,
) -> Element:
"""
Wait for a specific value in the current element, or for any non-empty value.
**Note:** The method requires the use of named arguments except ``expected_value``.
**Selenium & Playwright:**
- 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 expected_value: The value to waiting for. :obj:`None` - any value; :class:`str` - expected value.
:type expected_value: typing.Optional[str]
:param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`.
:type timeout: typing.Union[int, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
actual_value = self.value
if expected_value is not None:
result = actual_value == expected_value
error = f'Not expected value for "{self.name}"'
log_msg = f'Wait until value of "{self.name}" will be equal to "{expected_value}"'
else:
result = actual_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))
[docs] @wait_condition
def wait_enabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element:
"""
Wait for the element to become enabled and/or clickable.
**Note:** The method requires the use of named arguments.
**Selenium & Playwright:**
- 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: typing.Union[int, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
return Result(
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),
)
[docs] @wait_condition
def wait_disabled(self, *, timeout: float = WAIT_EL, silent: bool = False) -> Element:
"""
Wait for the element to become disabled.
**Note:** The method requires the use of named arguments.
**Selenium & Playwright:**
- 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, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
return Result(
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),
)
[docs] @wait_condition
def wait_for_size(
self,
expected_size: Size,
*,
timeout: float = WAIT_EL,
silent: bool = False,
) -> Element:
"""
Wait until element size will be equal to given :class:`.Size` object
**Note:** The method requires the use of named arguments except ``expected_size``.
**Selenium & Playwright:**
- 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 expected_size: expected element size
:type expected_size: :class:`.Size`
:param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`.
:type timeout: typing.Union[int, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
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(
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),
)
[docs] @wait_condition
def wait_elements_count(
self,
expected_count: int,
*,
timeout: float = WAIT_EL,
silent: bool = False,
) -> Element:
"""
Wait until the number of matching elements equals the expected count.
**Note:** The method requires the use of named arguments except ``expected_count``.
**Selenium & Playwright:**
- 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 expected_count: The expected number of elements.
:type expected_count: int
:param timeout: The maximum time to wait for the condition (in seconds). Default: :obj:`WAIT_EL`.
:type timeout: typing.Union[int, float]
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
actual_count = self.get_elements_count(silent=True)
error_msg = f'Unexpected elements count of "{self.name}"'
return Result(
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),
)
@property
def all_elements(self) -> list[Element] | list[Any]:
"""
Return a list of all matching elements.
:return: A list of wrapped :class:`Element` objects.
"""
if getattr(self, '_wrapped', None):
msg = f'all_elements property already used for {self.name}'
raise RecursionError(msg)
return self._base_cls.all_elements.fget(self)
[docs] def is_visible(self, check_displaying: bool = True, silent: bool = False) -> bool:
"""
Check if 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`.
:type check_displaying: bool
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`bool`
"""
if not silent:
self.log(f'Check visibility of "{self.name}"')
is_visible = True
if check_displaying:
is_visible = self.is_displayed()
if is_visible:
rect, window_size = self.get_rect(), self.driver_wrapper.get_inner_window_size()
x_end, y_end = rect['x'] + rect['width'], rect['y'] + rect['height']
is_start_visible = is_target_on_screen(x=rect['x'], y=rect['y'], possible_range=window_size)
is_end_visible = is_target_on_screen(x=x_end, y=y_end, possible_range=window_size)
is_visible = is_start_visible or is_end_visible
return is_visible
[docs] 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
: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`.
:type check_displaying: bool
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`bool`
"""
if not silent:
self.log(f'Check fully visibility of "{self.name}"')
is_visible = True
if check_displaying:
is_visible = self.is_displayed()
if is_visible:
rect, window_size = self.get_rect(), self.driver_wrapper.get_inner_window_size()
x_end, y_end = rect['x'] + rect['width'], rect['y'] + rect['height']
is_start_visible = is_target_on_screen(x=rect['x'], y=rect['y'], possible_range=window_size)
is_end_visible = is_target_on_screen(x=x_end, y=y_end, possible_range=window_size)
is_visible = is_start_visible and is_end_visible
return is_visible
[docs] def save_screenshot(
self,
file_name: str,
screenshot_base: bytes | Image = None,
convert_type: str | None = None,
) -> Image:
"""
Save a screenshot of the element.
:param file_name: Path or filename for the screenshot.
:type file_name: str
:param screenshot_base: Screenshot binary or image to use (optional).
:type screenshot_base: :obj:`bytes`, :class:`PIL.Image.Image`
:param convert_type: Image conversion type before saving (optional).
:type convert_type: str
:return: :class:`PIL.Image.Image`
"""
self.log(f'Save screenshot of {self.name}')
image_object = screenshot_base
if isinstance(screenshot_base, bytes) or screenshot_base is None:
image_object = self._base_cls.screenshot_image(self, screenshot_base)
if convert_type:
image_object = image_object.convert(convert_type)
image_object.save(file_name)
return image_object
[docs] def hide(self, silent: bool = False) -> Element:
"""
Make the element invisible by setting its opacity to 0.
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
if not silent:
self.log(f'Hiding element "{self.name}"')
self.execute_script('arguments[0].style.opacity = "0";')
return self
[docs] def show(self, silent: bool = False) -> Element:
"""
Make the element visible by setting its opacity to 1.
:param silent: If :obj:`True`, suppresses logging.
:type silent: bool
:return: :class:`Element`
"""
if not silent:
self.log(f'Showing element "{self.name}"')
self.execute_script('arguments[0].style.opacity = "1";')
return self
[docs] def execute_script(self, script: str, *args: Any) -> Any:
"""
Execute a JavaScript script on the element.
:param script: JavaScript code to be executed, referring to the element as ``arguments[0]``.
:type script: str
:param args: Any arguments to pass to the JavaScript.
:type args: :obj:`typing.Any`
:return: :obj:`typing.Any` result from the script.
"""
return self.driver_wrapper.execute_script(script, *[self, *list(args)])
[docs] def assert_screenshot(
self,
filename: str = '',
test_name: str = '',
name_suffix: str = '',
threshold: float | None = None,
delay: float | None = None,
scroll: bool = False,
remove: Element | list[Element] = None,
fill_background: str | bool = False,
cut_box: Box = None,
hide: Element | list[Element] = None,
) -> None:
"""
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.
:type filename: str
:param test_name: The custom test name for generated filename.
If empty - it will be determined automatically.
:type test_name: str
:param name_suffix: A suffix to add to the filename.
Useful for distinguishing between positive and negative cases for the same :class:`Element` during one test.
:type name_suffix: str
:param threshold: The acceptable threshold for comparing screenshots.
If :obj:`None` - takes default threshold or calculate its automatically based on screenshot size.
:type threshold: typing.Optional[int, float]
:param delay: The delay in seconds before taking the screenshot.
If :obj:`None` - takes default delay.
:type delay: typing.Optional[int, float]
:param scroll: Whether to scroll to the element before taking the screenshot.
:type scroll: bool
:param remove: :class:`Element` to remove from the screenshot.
Can be a single element or a list of elements.
:type remove: typing.Optional[Element or typing.List[Element]]
:param fill_background: The color to fill the background.
If :obj:`True`, uses a default color (black). If a :class:`str`, uses the specified color.
:type fill_background: typing.Optional[str or bool]
:param cut_box: A :class:`.Box` specifying a region to cut from the screenshot.
If :obj:`None`, no region is cut.
:type cut_box: typing.Optional[Box]
:param hide: :class:`Element` to hide in the screenshot.
Can be a single element or a list of elements.
:type hide: typing.Optional[Element or typing.List[Element]]
:return: :obj:`None`
"""
delay = delay or VisualComparison.default_delay
remove = [remove] if type(remove) is not list and remove else remove
if scroll:
self.scroll_into_view()
hide_before_screenshot(hide, is_optional=False, dw=self.driver_wrapper)
self.driver_wrapper.wait(delay)
hide_before_screenshot(VisualComparison.always_hide, is_optional=True, dw=self.driver_wrapper)
VisualComparison(self.driver_wrapper, self).assert_screenshot(
filename=filename,
test_name=test_name,
name_suffix=name_suffix,
threshold=threshold,
remove=remove,
fill_background=fill_background,
cut_box=cut_box,
)
reveal_after_screenshot(VisualComparison.always_hide, dw=self.driver_wrapper)
[docs] def soft_assert_screenshot(
self,
filename: str = '',
test_name: str = '',
name_suffix: str = '',
threshold: float | None = None,
delay: float | None = None,
scroll: bool = False,
remove: Element | list[Element] = None,
fill_background: str | bool = False,
cut_box: Box = None,
hide: Element | list[Element] = None,
) -> tuple[bool, str]:
"""
Compare the currently taken screenshot to the expected screenshot and return 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.
:type filename: str
:param test_name: The custom test name for generated filename.
If empty - it will be determined automatically.
:type test_name: str
:param name_suffix: A suffix to add to the filename.
Useful for distinguishing between positive and negative cases for the same :class:`Element` during one test.
:type name_suffix: str
:param threshold: The acceptable threshold for comparing screenshots.
If :obj:`None` - takes default threshold or calculate its automatically based on screenshot size.
:type threshold: typing.Optional[int, float]
:param delay: The delay in seconds before taking the screenshot.
If :obj:`None` - takes default delay.
:type delay: typing.Optional[int, float]
:param scroll: Whether to scroll to the element before taking the screenshot.
:type scroll: bool
:param remove: :class:`Element` to remove from the screenshot.
:type remove: typing.Optional[Element or typing.List[Element]]
:param fill_background: The color to fill the background.
If :obj:`True`, uses a default color (black). If a :class:`str`, uses the specified color.
:type fill_background: typing.Optional[str or bool]
:param cut_box: A :class:`.Box` specifying a region to cut from the screenshot.
If :obj:`None`, no region is cut.
:type cut_box: typing.Optional[Box]
:param hide: :class:`Element` to hide in the screenshot.
Can be a single element or a list of elements.
:return: :class:`typing.Tuple` (:class:`bool`, :class:`str`) - result state and result message
"""
try:
self.assert_screenshot(
filename,
test_name,
name_suffix,
threshold,
delay,
scroll,
remove,
fill_background,
cut_box,
hide,
)
except AssertionError as exc:
exc = str(exc)
self.log(exc, level=LogLevel.ERROR)
return False, exc
return True, f'No visual mismatch found for {self.name}'
[docs] def get_element_info(self, element: Element | None = None) -> str:
"""
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.
:type element: :class:`Element` or :obj:`None`
:return: :class:`str` - A string containing the log data.
"""
element = element or self
return get_element_info(element)
def _get_all_elements(self, sources: tuple | list) -> list[Any]:
"""
Retrieve all wrapped elements from the given sources.
:param sources: A list or tuple of source objects
:type sources: tuple or list
:return: A list of wrapped :class:`Element` objects.
"""
wrapped_elements = []
for element in sources:
wrapped_object: Any = copy(self)
wrapped_object.element = element
wrapped_object._wrapped = True
wrapped_object.sub_elements = dict(self.sub_elements)
set_parent_for_attr(wrapped_object, with_copy=True)
wrapped_elements.append(wrapped_object)
return wrapped_elements
def _modify_sub_elements(self) -> None:
"""
Initialize attributes with type == Element.
Required for classes with base == Element.
:return: :obj:`None`
"""
self.sub_elements = {}
if type(self) is not self._element_cls:
self.sub_elements = extract_named_objects(self, Element)
initialize_objects(self, self.sub_elements)
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
:return: :obj:`None`
"""
if not self._driver_wrapper_given:
PreviousObjectDriver().set_driver_from_previous_object(self)
@cached_property
def _element_cls(self) -> type[Element]:
"""
Returns the `Element` class.
This can be overridden for performance optimizations.
:return: :obj:`typing.Type` [:class:`Element`]
"""
return Element