From 787310a4108a73aed15e566e71ac4fb9e3c2d44b Mon Sep 17 00:00:00 2001
From: VladimirPodolian
Date: Thu, 16 Jul 2026 20:40:32 +0200
Subject: [PATCH 1/4] _set_static initial fixes
---
mops/base/driver_wrapper.py | 26 +++-
mops/base/element.py | 4 +-
mops/mixins/internal_mixin.py | 38 ++++--
tests/static_tests/conftest.py | 6 +-
.../integration/test_element_attrs.py | 126 ++++++++++++++++++
.../test_element_cross_driver_page.py | 105 +++++++++++++++
.../performance/test_overall_performance.py | 4 +-
7 files changed, 294 insertions(+), 15 deletions(-)
create mode 100644 tests/static_tests/integration/test_element_attrs.py
create mode 100644 tests/static_tests/integration/test_element_cross_driver_page.py
diff --git a/mops/base/driver_wrapper.py b/mops/base/driver_wrapper.py
index d77c13a4..f5bc4157 100644
--- a/mops/base/driver_wrapper.py
+++ b/mops/base/driver_wrapper.py
@@ -15,8 +15,6 @@
from mops.exceptions import DriverWrapperException
from mops.js_scripts import storage_get_items_js, storage_set_item_js
from mops.mixins.internal_mixin import InternalMixin
-from mops.mixins.objects.box import Box
-from mops.mixins.objects.driver import Driver
from mops.mixins.objects.visual_comaprison_mixin import hide_before_screenshot, reveal_after_screenshot
from mops.playwright.play_driver import PlayDriver
from mops.selenium.driver.mobile_driver import MobileDriver
@@ -37,6 +35,7 @@
class DriverWrapperSessions:
all_sessions: ClassVar[list[DriverWrapper]] = []
+ _MIN_SESSIONS_FOR_COMPARISON: ClassVar[int] = 2
@classmethod
def add_session(cls, driver_wrapper: DriverWrapper) -> None:
@@ -87,6 +86,29 @@ def is_connected(cls) -> bool:
"""
return any(cls.all_sessions)
+ @classmethod
+ def has_different_driver_types(cls) -> bool:
+ """
+ Check if the session pool contains different driver types
+ (e.g., Selenium + Appium, Selenium + Playwright).
+
+ :return: :obj:`bool` - :obj:`True` if at least two sessions use different driver types,
+ otherwise :obj:`False`.
+ """
+ if len(cls.all_sessions) < cls._MIN_SESSIONS_FOR_COMPARISON:
+ return False
+
+ driver_types = set()
+ for session in cls.all_sessions:
+ if session.is_playwright:
+ driver_types.add('playwright')
+ elif session.is_appium:
+ driver_types.add('appium')
+ elif session.is_selenium:
+ driver_types.add('selenium')
+
+ return len(driver_types) > 1
+
class DriverWrapper(InternalMixin, Logging, DriverWrapperABC):
"""
diff --git a/mops/base/element.py b/mops/base/element.py
index b9f6fcb4..0df9d827 100644
--- a/mops/base/element.py
+++ b/mops/base/element.py
@@ -66,7 +66,9 @@ def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) -> Ele
@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):
+ if getattr(self, '_initialized', False) and (
+ type(self) is cls or getattr(self, '_object', None) == 'group'
+ ):
self._modify_sub_elements()
cls.__init__ = wrapped_init
diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py
index 6e70f369..7aad74f5 100644
--- a/mops/mixins/internal_mixin.py
+++ b/mops/mixins/internal_mixin.py
@@ -9,6 +9,9 @@
is_driver_wrapper,
)
+_shadow_classes: dict[tuple[type, str], type] = {}
+_class_configured: dict[type, type] = {}
+
def get_element_info(element: Any, label: str = 'Selector=') -> str:
"""
@@ -58,7 +61,15 @@ def _safe_setter(self, var: str, value: Any) -> None:
def _get_protected_attrs(self: Any, current_obj_cls: type) -> set:
if not is_driver_wrapper(self):
- return set(get_all_static_attributes(current_obj_cls))
+ cached = current_obj_cls.__dict__.get('_pre_protected')
+
+ if cached is not None:
+ return set(cached)
+
+ protected = set(get_all_static_attributes(current_obj_cls))
+ current_obj_cls._pre_protected = frozenset(protected)
+
+ return protected
if '_framework_attrs' not in current_obj_cls.__dict__:
current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls))
@@ -67,22 +78,33 @@ def _get_protected_attrs(self: Any, current_obj_cls: type) -> set:
def _set_static(self: Any, cls: type) -> None:
"""
- Set static from base cls (Web/Mobile/Play Element/Page etc.)
+ Set attributes from base cls onto the class. Uses per-driver shadow
+ classes when multiple driver types are active.
:return: None
"""
- current_obj_cls = self.__class__
+ obj_cls = self.__class__
- if current_obj_cls.__dict__.get('_configured') is cls:
+ if _class_configured.get(obj_cls) is cls:
return
- protected = self._get_protected_attrs(current_obj_cls)
-
+ if not is_driver_wrapper(self) and self.driver_wrapper.session.has_different_driver_types():
+ original_cls = obj_cls
+ key = (original_cls, self.driver_wrapper._base_cls.__name__)
+ obj_cls = _shadow_classes.get(key)
+ if not obj_cls:
+ obj_cls = type(original_cls.__name__, (original_cls,), {'_shadow_class': True})
+ if '_pre_protected' in original_cls.__dict__:
+ obj_cls._pre_protected = original_cls.__dict__['_pre_protected']
+ _shadow_classes[key] = obj_cls
+ self.__class__ = obj_cls
+
+ protected = self._get_protected_attrs(obj_cls)
for name, value in get_static_attributes(cls).items():
if name not in protected:
- setattr(current_obj_cls, name, value)
+ setattr(obj_cls, name, value)
- current_obj_cls._configured = cls
+ _class_configured[obj_cls] = cls
def _repr_builder(self: Any) -> str | None:
class_name = self.__class__.__name__
diff --git a/tests/static_tests/conftest.py b/tests/static_tests/conftest.py
index 407adb65..79f95092 100644
--- a/tests/static_tests/conftest.py
+++ b/tests/static_tests/conftest.py
@@ -7,6 +7,7 @@
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver
from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions
+from mops.mixins.internal_mixin import _shadow_classes, _class_configured
from mops.playwright.play_driver import PlayDriver
from mops.selenium.core.core_driver import CoreDriver
@@ -163,8 +164,9 @@ def base_teardown():
except AttributeError:
pass
del MockedDriverWrapper._framework_attrs
- if '_configured' in MockedDriverWrapper.__dict__:
- del MockedDriverWrapper._configured
+ # Clean up caches to prevent cross-test leakage
+ _shadow_classes.clear()
+ _class_configured.clear()
mobile_drivers = [mocked_ios_driver.__name__, mocked_android_driver.__name__]
diff --git a/tests/static_tests/integration/test_element_attrs.py b/tests/static_tests/integration/test_element_attrs.py
new file mode 100644
index 00000000..2a0f4def
--- /dev/null
+++ b/tests/static_tests/integration/test_element_attrs.py
@@ -0,0 +1,126 @@
+"""Test that each driver type gets its own Element subclass in multi-driver mode."""
+import inspect
+
+from mops.base.element import Element
+from mops.selenium.elements.mobile_element import MobileElement
+from mops.selenium.elements.web_element import WebElement
+from mops.mixins.objects.driver import Driver
+from mock.mock import MagicMock
+from appium.webdriver.webdriver import WebDriver as AppiumDriver
+from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver
+from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions
+
+
+def _assert_method_from(element, name, source_cls):
+ raw = inspect.getattr_static(element, name, None)
+ assert raw is not None, f"'{name}' not found"
+ func = raw.__func__ if isinstance(raw, (classmethod, staticmethod)) else raw
+ qualname = getattr(func, '__qualname__', '')
+ assert source_cls.__name__ in qualname, (
+ f"'{name}' expected from {source_cls.__name__}, got {qualname!r}"
+ )
+
+
+class MockedDW(DriverWrapper):
+ pass
+
+
+def test_elements_get_own_driver_methods():
+ """Elements from the last driver type set the class-level methods."""
+ DriverWrapperSessions.all_sessions = []
+
+ # Mobile driver + element
+ appium_driver_cls = AppiumDriver
+ appium_driver_cls.__init__ = lambda *a, **kw: None
+ appium_driver_cls.session_id = None
+ appium_driver_cls.command_executor = MagicMock()
+ appium_driver_cls.error_handler = MagicMock()
+ appium_driver_cls.capabilities = MagicMock(return_value={
+ 'platformName': 'ios', 'browserName': 'safari', 'automationName': 'safari'
+ })()
+ mobile_dw = MockedDW(Driver(driver=appium_driver_cls()))
+
+ el_mobile = Element('m1', driver_wrapper=mobile_dw)
+ _assert_method_from(el_mobile, 'click_into_center', MobileElement)
+
+ # Web driver + element
+ selenium_driver_cls = SeleniumDriver
+ selenium_driver_cls.__init__ = lambda *a, **kw: None
+ selenium_driver_cls.session_id = None
+ selenium_driver_cls.command_executor = MagicMock()
+ selenium_driver_cls.error_handler = MagicMock()
+ selenium_driver_cls.caps = {}
+ web_dw = MockedDW(Driver(driver=selenium_driver_cls()))
+
+ el_web = Element('w1', driver_wrapper=web_dw)
+
+ # Each driver type gets its own cached shadow class
+ _assert_method_from(el_web, 'click_into_center', WebElement)
+
+ # Different driver types get different shadow classes when
+ # `has_different_driver_types` is active (multi-driver scenario)
+ assert type(el_mobile) is not type(el_web), 'Different drivers get different shadow classes'
+
+ DriverWrapperSessions.all_sessions = []
+
+
+def test_same_driver_elements_share_class():
+ """Elements from the same driver share the same subclass."""
+ DriverWrapperSessions.all_sessions = []
+
+ appium_driver_cls = AppiumDriver
+ appium_driver_cls.__init__ = lambda *a, **kw: None
+ appium_driver_cls.session_id = None
+ appium_driver_cls.command_executor = MagicMock()
+ appium_driver_cls.error_handler = MagicMock()
+ appium_driver_cls.capabilities = MagicMock(return_value={
+ 'platformName': 'ios', 'browserName': 'safari', 'automationName': 'safari'
+ })()
+ mobile_dw = MockedDW(Driver(driver=appium_driver_cls()))
+
+ selenium_driver_cls = SeleniumDriver
+ selenium_driver_cls.__init__ = lambda *a, **kw: None
+ selenium_driver_cls.session_id = None
+ selenium_driver_cls.command_executor = MagicMock()
+ selenium_driver_cls.error_handler = MagicMock()
+ selenium_driver_cls.caps = {}
+ MockedDW(Driver(driver=selenium_driver_cls()))
+
+ el1 = Element('a', driver_wrapper=mobile_dw)
+ el2 = Element('b', driver_wrapper=mobile_dw)
+
+ assert type(el1) is type(el2), (
+ f'Elements with same driver should share class: '
+ f'{type(el1).__name__} vs {type(el2).__name__}'
+ )
+
+ DriverWrapperSessions.all_sessions = []
+
+
+class CustomElement(Element):
+ custom_value = "custom_value"
+
+ @property
+ def value(self):
+ return self.custom_value
+
+
+def test_user_override_still_protected():
+ """User-defined overrides should still be protected."""
+ DriverWrapperSessions.all_sessions = []
+
+ appium_driver_cls = AppiumDriver
+ appium_driver_cls.__init__ = lambda *a, **kw: None
+ appium_driver_cls.session_id = None
+ appium_driver_cls.command_executor = MagicMock()
+ appium_driver_cls.error_handler = MagicMock()
+ appium_driver_cls.capabilities = MagicMock(return_value={
+ 'platformName': 'ios', 'browserName': 'safari', 'automationName': 'safari'
+ })()
+
+ dw = MockedDW(Driver(driver=appium_driver_cls()))
+ el = CustomElement('test', driver_wrapper=dw)
+
+ assert el.value == CustomElement.custom_value
+
+ DriverWrapperSessions.all_sessions = []
diff --git a/tests/static_tests/integration/test_element_cross_driver_page.py b/tests/static_tests/integration/test_element_cross_driver_page.py
new file mode 100644
index 00000000..fb25c407
--- /dev/null
+++ b/tests/static_tests/integration/test_element_cross_driver_page.py
@@ -0,0 +1,105 @@
+"""Test that Page elements get correct driver-specific methods."""
+import inspect
+
+from mops.base.element import Element
+from mops.base.page import Page
+from mops.selenium.elements.mobile_element import MobileElement
+from mops.selenium.elements.web_element import WebElement
+from mops.mixins.objects.driver import Driver
+from mock.mock import MagicMock
+from appium.webdriver.webdriver import WebDriver as AppiumDriver
+from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver
+from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions
+
+
+class Page1(Page):
+ element1 = Element('locator1', name='element1')
+
+
+class Page2(Page):
+ element2 = Element('locator2', name='element2')
+
+
+class MockedDW(DriverWrapper):
+ pass
+
+
+def _assert_method_from(element, name, source_cls):
+ raw = inspect.getattr_static(element, name, None)
+ assert raw is not None, f"'{name}' not found"
+ func = raw.__func__ if isinstance(raw, (classmethod, staticmethod)) else raw
+ qualname = getattr(func, '__qualname__', '')
+ assert source_cls.__name__ in qualname, (
+ f"'{name}' expected from {source_cls.__name__}, got {qualname!r}"
+ )
+
+
+def test_page_elements_different_drivers():
+ """Mobile first, then web: last driver type's methods are on the shared class."""
+ DriverWrapperSessions.all_sessions = []
+
+ # Mobile driver + page
+ appium_driver_cls = AppiumDriver
+ appium_driver_cls.__init__ = lambda *a, **kw: None
+ appium_driver_cls.session_id = None
+ appium_driver_cls.command_executor = MagicMock()
+ appium_driver_cls.error_handler = MagicMock()
+ appium_driver_cls.capabilities = MagicMock(return_value={
+ 'platformName': 'ios', 'browserName': 'safari', 'automationName': 'safari'
+ })()
+ mobile_dw = MockedDW(Driver(driver=appium_driver_cls()))
+
+ page1 = Page1(driver_wrapper=mobile_dw)
+ _assert_method_from(page1.element1, 'click_into_center', MobileElement)
+
+ # Web driver + page
+ selenium_driver_cls = SeleniumDriver
+ selenium_driver_cls.__init__ = lambda *a, **kw: None
+ selenium_driver_cls.session_id = None
+ selenium_driver_cls.command_executor = MagicMock()
+ selenium_driver_cls.error_handler = MagicMock()
+ selenium_driver_cls.caps = {}
+ web_dw = MockedDW(Driver(driver=selenium_driver_cls()))
+
+ page2 = Page2(driver_wrapper=web_dw)
+
+ _assert_method_from(page2.element2, 'click_into_center', WebElement)
+ # Elements share the same class; the last driver (web) sets class-level methods.
+ # Instance-level data (locator, name) is per-instance.
+
+ DriverWrapperSessions.all_sessions = []
+
+
+def test_page_elements_web_first_then_mobile():
+ """Web first, then mobile: last driver type's methods are on the shared class."""
+ DriverWrapperSessions.all_sessions = []
+
+ # Web driver + page
+ selenium_driver_cls = SeleniumDriver
+ selenium_driver_cls.__init__ = lambda *a, **kw: None
+ selenium_driver_cls.session_id = None
+ selenium_driver_cls.command_executor = MagicMock()
+ selenium_driver_cls.error_handler = MagicMock()
+ selenium_driver_cls.caps = {}
+ web_dw = MockedDW(Driver(driver=selenium_driver_cls()))
+
+ page1 = Page1(driver_wrapper=web_dw)
+ _assert_method_from(page1.element1, 'click_into_center', WebElement)
+
+ # Mobile driver + page
+ appium_driver_cls = AppiumDriver
+ appium_driver_cls.__init__ = lambda *a, **kw: None
+ appium_driver_cls.session_id = None
+ appium_driver_cls.command_executor = MagicMock()
+ appium_driver_cls.error_handler = MagicMock()
+ appium_driver_cls.capabilities = MagicMock(return_value={
+ 'platformName': 'ios', 'browserName': 'safari', 'automationName': 'safari'
+ })()
+ mobile_dw = MockedDW(Driver(driver=appium_driver_cls()))
+
+ page2 = Page2(driver_wrapper=mobile_dw)
+
+ _assert_method_from(page2.element2, 'click_into_center', MobileElement)
+ # Elements share the same class; the last driver (mobile) sets class-level methods.
+
+ DriverWrapperSessions.all_sessions = []
diff --git a/tests/static_tests/performance/test_overall_performance.py b/tests/static_tests/performance/test_overall_performance.py
index a0d873e2..15928cf7 100644
--- a/tests/static_tests/performance/test_overall_performance.py
+++ b/tests/static_tests/performance/test_overall_performance.py
@@ -91,10 +91,10 @@ def test_performance_element_initialisation(mocked_selenium_driver, case, set_el
expected_peak_mem = 4.0
expected_init_duration = 0.4
if sys.version_info >= (3, 12):
- expected_peak_mem = 3.8
+ expected_peak_mem = 4.1
expected_init_duration = 0.4
if sys.version_info >= (3, 13):
- expected_peak_mem = 4.0
+ expected_peak_mem = 4.2
expected_init_duration = 0.4
assert init_without_profiling_stop_timestamp < init_without_profiling_expected,\
From 5f0e77656ec8266bd3970c60d2e7046e73484243 Mon Sep 17 00:00:00 2001
From: VladimirPodolian
Date: Thu, 16 Jul 2026 21:18:00 +0200
Subject: [PATCH 2/4] Fixes
---
mops/base/driver_wrapper.py | 2 +-
mops/base/element.py | 6 ++---
mops/base/page.py | 2 +-
mops/mixins/internal_mixin.py | 48 +++++++++++++++++------------------
4 files changed, 28 insertions(+), 30 deletions(-)
diff --git a/mops/base/driver_wrapper.py b/mops/base/driver_wrapper.py
index f5bc4157..2ccc147d 100644
--- a/mops/base/driver_wrapper.py
+++ b/mops/base/driver_wrapper.py
@@ -500,7 +500,7 @@ def __init_base_class__(self) -> None:
)
raise DriverWrapperException(msg)
- self._set_static(self._base_cls)
+ self._set_static(self._base_cls, with_shadow=False)
self._base_cls.__init__(self, driver_container=self.__driver_container)
for name, value in self.__dict__.items():
diff --git a/mops/base/element.py b/mops/base/element.py
index 0df9d827..80f1d348 100644
--- a/mops/base/element.py
+++ b/mops/base/element.py
@@ -66,9 +66,7 @@ def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs: Any) -> Ele
@functools.wraps(orig_init)
def wrapped_init(self: Any, *args: Any, **kw: Any) -> None:
orig_init(self, *args, **kw)
- if getattr(self, '_initialized', False) and (
- type(self) is cls or getattr(self, '_object', None) == 'group'
- ):
+ if getattr(self, '_initialized', False):
self._modify_sub_elements()
cls.__init__ = wrapped_init
@@ -183,7 +181,7 @@ def __init_base_class__(self) -> None:
)
raise DriverWrapperException(msg)
- self._set_static(self._base_cls)
+ self._set_static(self._base_cls, with_shadow=True)
self._base_cls.__init__(self)
self._initialized = True
diff --git a/mops/base/page.py b/mops/base/page.py
index b1e59d03..9f77d261 100644
--- a/mops/base/page.py
+++ b/mops/base/page.py
@@ -108,7 +108,7 @@ def __init_base_class__(self) -> None:
)
raise DriverWrapperException(msg)
- self._set_static(self._base_cls)
+ self._set_static(self._base_cls, with_shadow=True)
self._base_cls.__init__(self)
@cached_property
diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py
index 7aad74f5..621d3ae8 100644
--- a/mops/mixins/internal_mixin.py
+++ b/mops/mixins/internal_mixin.py
@@ -6,7 +6,6 @@
from mops.utils.internal_utils import (
extract_all_named_objects,
extract_named_objects,
- is_driver_wrapper,
)
_shadow_classes: dict[tuple[type, str], type] = {}
@@ -60,23 +59,12 @@ def _safe_setter(self, var: str, value: Any) -> None:
setattr(self, var, value)
def _get_protected_attrs(self: Any, current_obj_cls: type) -> set:
- if not is_driver_wrapper(self):
- cached = current_obj_cls.__dict__.get('_pre_protected')
-
- if cached is not None:
- return set(cached)
-
- protected = set(get_all_static_attributes(current_obj_cls))
- current_obj_cls._pre_protected = frozenset(protected)
-
- return protected
-
if '_framework_attrs' not in current_obj_cls.__dict__:
current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls))
return current_obj_cls.__dict__['_framework_attrs']
- def _set_static(self: Any, cls: type) -> None:
+ def _set_static(self: Any, cls: type, with_shadow: bool) -> None:
"""
Set attributes from base cls onto the class. Uses per-driver shadow
classes when multiple driver types are active.
@@ -88,24 +76,36 @@ def _set_static(self: Any, cls: type) -> None:
if _class_configured.get(obj_cls) is cls:
return
- if not is_driver_wrapper(self) and self.driver_wrapper.session.has_different_driver_types():
- original_cls = obj_cls
- key = (original_cls, self.driver_wrapper._base_cls.__name__)
- obj_cls = _shadow_classes.get(key)
- if not obj_cls:
- obj_cls = type(original_cls.__name__, (original_cls,), {'_shadow_class': True})
- if '_pre_protected' in original_cls.__dict__:
- obj_cls._pre_protected = original_cls.__dict__['_pre_protected']
- _shadow_classes[key] = obj_cls
- self.__class__ = obj_cls
-
protected = self._get_protected_attrs(obj_cls)
+
+ if with_shadow and self.driver_wrapper.session.has_different_driver_types():
+ obj_cls = self._set_shadow_class(protected)
+
for name, value in get_static_attributes(cls).items():
if name not in protected:
setattr(obj_cls, name, value)
_class_configured[obj_cls] = cls
+ def _set_shadow_class(self, attrs: dict) -> type:
+ """
+ Set shadow class to given object
+
+ :param attrs: attrs to set for newly created class
+ :return: type object
+ """
+ original_cls = self.__class__
+ key = (original_cls, self.driver_wrapper._base_cls.__name__)
+ obj_cls = _shadow_classes.get(key)
+ if not obj_cls:
+ data = {'_shadow_class': True, '_framework_attrs': attrs}
+ obj_cls = type(original_cls.__name__, (original_cls,), data)
+ _shadow_classes[key] = obj_cls
+
+ self.__class__ = obj_cls
+
+ return obj_cls
+
def _repr_builder(self: Any) -> str | None:
class_name = self.__class__.__name__
obj_id = hex(id(self))
From df198a4f0b2cd7acc6d125252c56daa4bdd4ffc6 Mon Sep 17 00:00:00 2001
From: VladimirPodolian
Date: Thu, 16 Jul 2026 21:24:50 +0200
Subject: [PATCH 3/4] Fixes
---
mops/mixins/internal_mixin.py | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/mops/mixins/internal_mixin.py b/mops/mixins/internal_mixin.py
index 621d3ae8..59114fa2 100644
--- a/mops/mixins/internal_mixin.py
+++ b/mops/mixins/internal_mixin.py
@@ -49,6 +49,7 @@ def get_driver_instance(driver_type: type, instance: type) -> bool:
class InternalMixin:
driver: None
+ driver_wrapper: None
def _driver_is_instance(self, instance: type) -> bool:
"""Check if the current driver is an instance of the given type."""
@@ -58,9 +59,9 @@ def _safe_setter(self, var: str, value: Any) -> None:
if not hasattr(self, var):
setattr(self, var, value)
- def _get_protected_attrs(self: Any, current_obj_cls: type) -> set:
+ def _get_protected_attrs(self: Any, current_obj_cls: type) -> frozenset:
if '_framework_attrs' not in current_obj_cls.__dict__:
- current_obj_cls._framework_attrs = set(get_all_static_attributes(current_obj_cls))
+ current_obj_cls._framework_attrs = frozenset(get_all_static_attributes(current_obj_cls))
return current_obj_cls.__dict__['_framework_attrs']
@@ -87,19 +88,24 @@ def _set_static(self: Any, cls: type, with_shadow: bool) -> None:
_class_configured[obj_cls] = cls
- def _set_shadow_class(self, attrs: dict) -> type:
+ def _set_shadow_class(self, protected: frozenset) -> type:
"""
- Set shadow class to given object
+ Create or reuse a per-driver shadow subclass. The given *protected*
+ set was computed from the original class before any attributes were
+ injected, so the shadow class starts with the same baseline.
- :param attrs: attrs to set for newly created class
- :return: type object
+ :param protected: pre-computed _framework_attrs of the original class.
+ :return: the shadow class (a subtype of the original class).
"""
original_cls = self.__class__
key = (original_cls, self.driver_wrapper._base_cls.__name__)
obj_cls = _shadow_classes.get(key)
if not obj_cls:
- data = {'_shadow_class': True, '_framework_attrs': attrs}
- obj_cls = type(original_cls.__name__, (original_cls,), data)
+ obj_cls = type(
+ original_cls.__name__,
+ (original_cls,),
+ {'_shadow_class': True, '_framework_attrs': protected},
+ )
_shadow_classes[key] = obj_cls
self.__class__ = obj_cls
From 34e7c38d502d834572665941120c0a6f0a79a712 Mon Sep 17 00:00:00 2001
From: VladimirPodolian
Date: Thu, 16 Jul 2026 21:38:50 +0200
Subject: [PATCH 4/4] Fixes
---
CHANGELOG.md | 326 +++++++++---------
README.md | 2 +-
mops/__init__.py | 2 +-
.../performance/test_overall_performance.py | 48 +--
4 files changed, 181 insertions(+), 197 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f903af9..af12ebab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,20 @@
-# Mops Changelog
+## Mops Changelog
-## v3.5.2
+### v3.5.3
-### Added
+*Release date: 2026-07-16*
+
+#### Fixed
+- Multi-driver scenarios: per-driver shadow subclasses isolate driver-specific methods when multiple driver
+types are active, preventing cross-driver method overwrites on shared Element/Page classes.
+
+---
+
+### v3.5.2
+
+#### Added
- `DriverWrapper.is_cdp` flag to identify CDP-connected driver instances
- `PlayDriver.quit` graceful error handling for CDP contexts; tracing skip when `is_cdp` is set
- `CoreDriver.quit` graceful error handling for externally-managed browsers when `is_cdp` is set
@@ -12,26 +22,26 @@
---
-## v3.5.1
+### v3.5.1
*Release date: 2026-06-23*
-### Changed
+#### Changed
- Selenium & Playwright: exponential delay starting at 0.1 seconds between requests for all waiting methods
---
-## v3.5.0
+### v3.5.0
*Release date: 2026-04-22*
-### Breaking Changes
+#### Breaking Changes
- **Python 3.8 and 3.9 dropped** — minimum supported version is now Python 3.10
-### Added
+#### Added
- **Ruff linter** integrated: new CI workflow (`ruff.yml`), pre-commit hook, and full config in `pyproject.toml`
- **Python 3.13 and 3.14** support
-### Changed
+#### Changed
- `playwright` bumped `>=1.58.0`
- `numpy` bumped to `>=2.3.2`
- `opencv-python` bumped to `>=4.13.0`
@@ -41,10 +51,10 @@
---
-## v3.4.3
+### v3.4.3
*Release date: 2026-04-21*
-### Added
+#### Added
- `set_local_storage_item(items)` / `set_session_storage_item(items)` — set one or more key/value pairs in localStorage / sessionStorage
- `get_local_storage_item(key)` / `get_session_storage_item(key)` — retrieve a single item by key (`None` if missing)
- `get_local_storage_items()` / `get_session_storage_items()` — retrieve all items as a dict
@@ -53,47 +63,47 @@
- `MobileDriver.clear_cookies` override — on iOS real devices iterates and deletes each cookie individually via `delete_cookie` instead of a bulk clear
- `storage_set_item_js`, `storage_get_items_js`, `set_cookies_as_batch_js` JS helpers added to `js_scripts.py`
-### Changed
+#### Changed
- `CoreDriver.set_cookies` — replaced per-cookie `driver.add_cookie()` loop with a single batched `execute_script` call
- `PlayDriver.set_cookies` — domain extracted via `urlparse` instead of manual string splitting; cookie defaults applied via list comprehension
- `PlayDriver.execute_script` — unified script wrapping to `(args) => (function() { … }).apply(null, args)` for consistent argument passing across all call patterns
---
-## v3.4.2
+### v3.4.2
*Release date: 2026-03-28*
-### Fixed
+#### Fixed
- `ShadowDriverWrapper` now correctly receives static methods of its driver type — previously methods from the first session's driver were inherited and not overridden
- `get_driver_instance` cache key changed from driver instance to driver type — prevents cache misses on every new driver object
-### Changed
+#### Changed
- `_set_static` guard stores the configured class instead of `True` — allows re-configuration when driver type changes
- `_set_static` uses `_framework_attrs` snapshot instead of full MRO scan — protects only original framework methods, not previously set driver-specific ones
---
-## v3.4.1
+### v3.4.1
*Release date: 2026-03-27*
-### Fixed
+#### Fixed
- Element.locator/locator_type/log_locator access without initialised driver
-## v3.4.0 (Performance improvement)
+### v3.4.0 (Performance improvement)
*Release date: 2026-03-27*
-### Breaking Changes
+#### Breaking Changes
- **`Group` subclasses**: `parent` is now correctly set on sub-elements defined after `super().__init__()` —
previously such elements did not receive `parent` argument
-### Added
+#### Added
- `Element.sub_elements` dict — collected once and reused instead of rescanning on every access
- `ElementMeta` metaclass — triggers `_modify_sub_elements` automatically after `__init__` of the final class
- `get_static_attributes` / `get_all_static_attributes` with `lru_cache` — replaces repeated attribute scanning
- `get_driver_instance` with `lru_cache` — caches `isinstance` results for driver type checks
- `_driver_is_instance` method on `InternalMixin` — single cached entry point for driver type detection
-### Changed
+#### Changed
- `all_tags` converted to `frozenset` for O(1) membership checks
- `initialize_objects` no longer recurses manually — delegates to `_modify_sub_elements` on each child
- `set_parent_for_attr` uses `sub_elements` dict instead of rescanning object attributes
@@ -102,31 +112,31 @@ previously such elements did not receive `parent` argument
- `__copy__` added to `Element` for explicit shallow copy control
- `__getattribute__` override removed from `Element` — initialization guard moved to `CoreElement`/`PlayElement`
-### Fixed
+#### Fixed
- Error messages for unsupported driver type now include the actual driver class name and list expected types
---
-## v3.3.2
+### v3.3.2
*Release date: 2026-03-24*
-### Added
+#### Added
- `Element.source_locator` attribute that preserves the original locator before platform-specific transformations
---
-## v3.3.1
+### v3.3.1
*Release date: 2026-01-05*
-### Changed
+#### Changed
- `safe_call` exceptions list
---
-## v3.3.0
+### v3.3.0
*Release date: 2026-01-05*
-### Added
+#### Added
- `DriverWrapper.wait`: `reason` arg
- `DriverWrapper.get_scroll_position` method
- `Element.show` method
@@ -134,11 +144,11 @@ previously such elements did not receive `parent` argument
- `Locator.tap` instead of `Locator.click` for playwright on mobile resolution
- `WebElement` methods retry logic on `JavascriptException`
-### Changed
+#### Changed
- Playwright's `scroll_into_view` logic moved to js (same as Selenium)
- `NativeSafari` locators
-### Fixed
+#### Fixed
- Parent element cache drop
---
@@ -146,45 +156,45 @@ previously such elements did not receive `parent` argument
## Patch: v3.1.1, v3.2.1
*Release date: 2025-07-18*
-### Added
+#### Added
- Appium related locator types in `LocatorType` class
-### Changed
+#### Changed
- Selenium only: `DriverWrapper.set_window_size` inner viewport calculation skipped for mobile resolution
-### Removed
+#### Removed
- Automatic locator type detection for text based locators
-### Fixed
+#### Fixed
- Visual comparison dummy element class name
- Visual comparison error traceback
---
-## v3.2.0
+### v3.2.0
*Release date: 2025-03-31*
-### Added
+#### Added
- Continuous verification waits for `Element.wait_visibility` and `Element.wait_hidden`
- `ContinuousWaitException` for continuous verification errors in wait methods
- Caret hiding for screenshots in Selenium / Appium
- `is_displayed` / `is_hidden` execution results logged
- Retry decorator for Selenium / Appium methods to handle `StaleElementReferenceException`
-### Changed
+#### Changed
- Playwright's `wait_visibility`, `wait_hidden`, and `wait_availability` logic moved to MOPS methods
-### Fixed
+#### Fixed
- Small bug in the documentation example
- Bug in the `assert_screenshot` method with the `remove` argument for certain popups
- `StaleElementReferenceException` handler during wait/gathering of `Element.text`
---
-## v3.1.0
+### v3.1.0
*Release date: 2025-01-29*
-### Added
+#### Added
- [Kitchen Sink](https://mops.readthedocs.io/3.1.0/kitchen_sink/index.html) section on ReadTheDocs
- Source code links for interfaces in ReadTheDocs [< example > ](https://mops.readthedocs.io/3.1.0/_modules/mops/base/driver_wrapper.html#DriverWrapper.save_screenshot)
- `PlayDriver.delete_cookie` method
@@ -192,7 +202,7 @@ previously such elements did not receive `parent` argument
- `PlayDriver.switch_to_default_content` method
- `LocatorType` constants [< more info >](https://mops.readthedocs.io/3.1.0/kitchen_sink/locator_type.html)
-### Changed
+#### Changed
- **Breaking:** Minimum Playwright version is now [1.48.0](https://pypi.org/project/playwright/1.48.0/)
- **Breaking:** `DriverWrapper.get_inner_window_size` now returns a `Size` object instead of a `tuple`
- **Breaking:** `DriverWrapper.set_window_size` now sets the inner window size (affects Selenium only)
@@ -204,39 +214,39 @@ previously such elements did not receive `parent` argument
- Selenium & Appium error logging
- Improved most docstrings
-### Fixed
+#### Fixed
- `Element.wait_for_value` now correctly handles an empty `expected_value` argument
- `Element.wait_for_text` now correctly handles an empty `expected_text` argument
- `Element.value` now returns an empty string instead of `None`
-### Removed
+#### Removed
- **Breaking:** Removed `Locator.loc_type` attribute/argument [< how to provide locator type >](https://mops.readthedocs.io/3.1.0/kitchen_sink/locator_type.html)
- **Breaking:** Removed `CutBox` dataclass [< use Box instead >](https://mops.readthedocs.io/3.1.0/kitchen_sink/box.html)
-### Reworked
+#### Reworked
- Unified selector synchronization methods across all frameworks
- Improved automatic locator type detection
---
-## v3.0.0
+### v3.0.0
*Release date: 2025-01-13*
-### Breaking: Project renamed
+#### Breaking: Project renamed
-### Changed
+#### Changed
- Dev: pyproject.toml integrated instead of setup.py
- Dev: UV integrated
-### Removed
+#### Removed
- Dev: tox usage removed
---
-## v2.4.0
+### v2.4.0
*Release date: 2025-01-10*
-### Added
+#### Added
- `DriverWrapper.is_safari` method
- `DriverWrapper.is_firefox` method
- `DriverWrapper.is_chrome` method
@@ -244,11 +254,11 @@ previously such elements did not receive `parent` argument
- `VisualComparison` now removes actual and diff files upon successful assertions
- Internal CI pipelines with tests
-### Removed
+#### Removed
- `DriverWrapper.switch_to_parent_frame` method
- `dyatel/dyatel_play/helpers/trace.py` module
-### Changed
+#### Changed
- `DriverWrapper.get_inner_window_size` now returns a `Size` object instead of a dictionary
- `Element.click` now uses a JavaScript click for the Safari driver
- `Element.click_outside` now has default arguments `x=-5, y=-5` across all platforms
@@ -259,7 +269,7 @@ previously such elements did not receive `parent` argument
- The Safari browser on Selenium uses JavaScript-based `click` instead of the driver's `click` API to enhance compatibility with other platforms
- Documentation for most methods has been improved
-### Fixed
+#### Fixed
- `DriverWrapper.save_screenshot` no longer throws an error when called without the optional `screenshot_base` argument
- `Element.save_screenshot` no longer throws an error when called without the optional `screenshot_base` argument
- `VisualComparison.assert_screenshot` with the argument `threshold=0` now respects the provided value instead of defaulting
@@ -267,13 +277,13 @@ previously such elements did not receive `parent` argument
---
-## v2.3.3
+### v2.3.3
*Release date: 2025-01-09*
-### Added
+#### Added
- `dyatel.mixins/objects.driver.Driver` object
-### Changed
+#### Changed
- **Breaking:** `DriverWrapper` initialization now requires a `Driver` object
- **Breaking:** Playwright's `context` and `page` creation have been moved out of `dyatel-wrapper`
@@ -281,10 +291,10 @@ previously such elements did not receive `parent` argument
---
-## v2.3.2
+### v2.3.2
*Release date: 2024-12-19*
-### Changed
+#### Changed
- Supported Python-Appium-Client version changed from `2.11.1` to `3.1.0`
- Supported appium version changed from `2.2.1` to `2.12.1`
- Supported xcuitest version changed from `5.0.0` to `7.28.3`
@@ -292,18 +302,18 @@ previously such elements did not receive `parent` argument
---
-## v2.3.1
+### v2.3.1
*Release date: 2024-12-16*
-### Fixed
+#### Fixed
- Memory leak due to misuse of lru_cache
---
-## v2.3.0
+### v2.3.0
*Release date: 2024-09-12*
-### Added
+#### Added
- [ReadTheDocs documentation](https://dyatel-wrapper.readthedocs.io/)
- `Locator` object
- `CutBox` object
@@ -315,7 +325,7 @@ previously such elements did not receive `parent` argument
- Playwright only: 0.1 seconds delay between requests for a few waiting methods
- Appium only: exponential delay starting at 0.1 seconds between requests for all waiting methods
-### Changed
+#### Changed
- **Breaking:** `locator_type`, `mobile`, `ios`, `android`, and `desktop` kwargs removed
- **Breaking:** Most `Element` methods have been renamed
- **Breaking:** `DriverWrapper.execute_script` now uses the `Element` object instead of the source element object
@@ -327,58 +337,58 @@ previously such elements did not receive `parent` argument
- Selenium & Appium only: `Element.click` now retries on `ElementNotInteractableException`, `ElementClickInterceptedException`, `StaleElementReferenceException` exceptions
- Automatically generated `name` argument, based on the attribute name, has been removed
-### Fixed
+#### Fixed
- Playwright: Appending of dummy elements inside `Element.assert_screenshot`
- Playwright: `DriverWrapper.execute_script` error when multiple elements are available
-### Reworked
+#### Reworked
- Mobile `top_bar_height` and `bottom_bar_height` now use `NativeContext` & `NativeSafari` objects
- Most `Element` `wait` methods are now resolved with the `wait_condition` decorator
---
-## v2.2.15
+### v2.2.15
*Release date: 2024-08-13*
-### Added
+#### Added
- Python 3.11 and 3.12 support
---
-## v2.2.14
+### v2.2.14
*Release date: 2024-07-24*
-### Added
+#### Added
- assert_screenshot: possibility to hide objects before taking screenshot
- assert_screenshot: the diff image save on a different sized screenshots (reference/output)
- assert_screenshot: allure attachments increased for some cases
- assert_screenshot: auto label `mobile` for mobile resolution screenshots
-### Fixed
+#### Fixed
- assert_screenshot: bug with default.png for screenshots with given names fixed
---
-## v2.2.12 & v2.2.13
+### v2.2.12 & v2.2.13
*Release date: 2024-06-08*
-### Added
+#### Added
- iPad support
---
-## v2.2.11
+### v2.2.11
*Release date: 2024-05-29*
-### Changed
+#### Changed
- Minimum playwright version is 1.41.0
---
-## v2.2.1
+### v2.2.1
*Release date: 2024-04-16*
-### Added
+#### Added
- `DriverWrapper.is_tablet`
- `DriverWrapper.is_appium`
- `DriverWrapper.is_ios_tablet`
@@ -386,16 +396,16 @@ previously such elements did not receive `parent` argument
- `DriverWrapper.is_android_tablet`
- `DriverWrapper.is_android_mobile`
-### Fixed
+#### Fixed
- `Element.is_fully_visible` calculation
- `Element.is_visible` calculation
---
-## v2.2.0
+### v2.2.0
*Release date: 2024-03-04*
-### Added
+#### Added
- `Element.size` method
- `Element.location` method
- `Element.wait_element_size` method
@@ -404,105 +414,105 @@ previously such elements did not receive `parent` argument
- `DriverWrapper.wait` method
- `DriverWrapper/Element.screenshot_image` method
-### Changed
+#### Changed
- `DriverWrapper/Element.screenshot_base` method now return image binary
- `DriverWrapper/Element.save_screenshot` method now saves screenshot and moved to base class
- iOS only: `DriverWrapper.screenshot_base` returns image binary without native controls
- iOS only: `Element.screenshot_base` screenshot size for some elements could be changed
-### Fixed
+#### Fixed
- Type annotations
- `Element.is_visible/is_fully_visible` calculation
- iOS only: `Element.get_bottom_bar_height` calculation
---
-## v2.1.9
+### v2.1.9
*Release date: 2024-02-22*
-### Added
+#### Added
- Playwright `context.tracing` support
---
-## v2.1.8
+### v2.1.8
*Release date: 2024-01-04*
-### Added
+#### Added
- Playwright `new_context` args supports
---
-## v2.1.7
+### v2.1.7
*Release date: 2023-12-05*
-### Added
+#### Added
- VisualComparison: Dynamic threshold calculation
---
-## v2.1.6
+### v2.1.6
*Release date: 2023-11-26*
-### Fixed
+#### Fixed
- Performance fixes for session with 2 or more browser windows
---
-## v2.1.5
+### v2.1.5
*Release date: 2023-10-17*
-### Fixed
+#### Fixed
- Typo fix inside `MobileDriver`
---
-## v2.1.4
+### v2.1.4
*Release date: 2023-10-16*
-### Fixed
+#### Fixed
- Internal usage of Element class inside DriverWrapper
-### Changed
+#### Changed
- AssertionError output of visual comparison
-### Added
+#### Added
- Soft visual reference generation
- Soft assert screenshot
- `LogLevel` class
---
-## v2.1.3
+### v2.1.3
*Release date: 2023-09-10*
-### Changed
+#### Changed
- Selenium/Appium only: Additional logging for element enabled
- Selenium/Appium element gathering and exceptions reworked
---
-## v2.1.2
+### v2.1.2
*Release date: 2023-09-07*
-### Fixed
+#### Fixed
- Additional logging for element disabled
---
-## v2.1.1
+### v2.1.1
*Release date: 2023-09-07*
-### Fixed
+#### Fixed
- `setup.py` packages
---
-## v2.1.0
+### v2.1.0
*Release date: 2023-09-07*
-### Added
+#### Added
- Abstract classes and methods
- `DriverWrapperSessions` class
- `DriverWrapper.browser_name` attribute
@@ -510,10 +520,10 @@ previously such elements did not receive `parent` argument
- `Element.scroll_into_view` 'block' argument validation
- Selenium/Appium only: additional warning for `element` errors
-### Fixed
+#### Fixed
- Type annotations for some methods
-### Changed
+#### Changed
- `Page.anchor` property now instance attribute
- Some methods moved to subclasses
- Internal `Logging` reworked
@@ -521,10 +531,10 @@ previously such elements did not receive `parent` argument
---
-## v2.0.0
+### v2.0.0
*Release date: 2023-04-06*
-### Added
+#### Added
- `element.wait_enabled` method
- `element.wait_disabled` method
- `element.is_enabled` method
@@ -535,13 +545,13 @@ previously such elements did not receive `parent` argument
- `DriverWrapper.dismiss_alert` method (Selenium Only)
- `MobileDriver.click_in_alert` method (Appium Only)
-### Fixed
+#### Fixed
- MRO for Mobile + Desktop session
- Rapidly requests for current context on mobile
- `element.all_elements` recursion
- logging stderr to stdout
-### Changed
+#### Changed
- Checkbox class removed (all methods in Element class)
- New screenshot comparison engine. By: [@laruss](https://github.com/laruss)
- Elements initialization
@@ -551,62 +561,62 @@ previously such elements did not receive `parent` argument
---
-## v1.3.4
+### v1.3.4
*Release date: 2023-01-17*
-### Fixed
+#### Fixed
- Error logs fixes
---
-## v1.3.3
+### v1.3.3
*Release date: 2023-01-12*
-### Changed
+#### Changed
- `element.assert_screenshot` elements removal rework
---
-## v1.3.2
+### v1.3.2
*Release date: 2022-12-08*
-### Added
+#### Added
- mobile `element.hide_keyboard` method added
- `fill_background` arg in `element.assert_screenshot`
-### Changed
+#### Changed
- ios safaridriver support removed
- reruns disabling for visual tests without references
-### Fixed
+#### Fixed
- Pillow warning fixes
- other fixes and improvements
---
-## v1.3.1
+### v1.3.1
*Release date: 2022-12-02*
-### Added
+#### Added
- `element.wait_element_hidden_without_error` method
- `element.assert_screenshot` hard reference generation support
- `element.assert_screenshot` soft reference generation fix
- `element.hover` silent argument
-### Changed
+#### Changed
- Reworked wait argument for `element`: False - wait element hidden; True - wait element visible
- `page.is_page_opened` without url support
- selenium - tags (locator type) updated
-### Fixed
+#### Fixed
- DifferentDriverWrapper and elements initialization fixes
---
-## v1.3.0
+### v1.3.0
*Release date: 2022-10-18*
-### Added
+#### Added
- `driver_wrapper.get_inner_window_size` method
- `driver_wrapper.switch_to_frame` method for selenium based driver
- `driver_wrapper.switch_to_parent_frame` method for selenium based driver
@@ -621,12 +631,12 @@ previously such elements did not receive `parent` argument
- Auto implemented `parent` in hidden object (function/property etc.) for `element/checkbox`
- Platform specific locator by object kwargs: ios/android/mobile/desktop
-### Changed
+#### Changed
- `element.get_rect` for selenium desktop
- All visual comparisons staff moved to `VisualComparison` class
- Logging
-### Fixed
+#### Fixed
- `get_object_kwargs` function
- `initialize_objects_with_args` function
- `element.assert_screenshot` driver name for remote
@@ -634,77 +644,77 @@ previously such elements did not receive `parent` argument
---
-## v1.2.8
+### v1.2.8
*Release date: 2022-09-20*
-### Added
+#### Added
- `driver_wrapper.is_native_context` property on mobile
- `driver_wrapper.is_web_context` property on mobile
- `driver_wrapper.visual_reference_generation` that disable AssertionError exception in `element.assert_screenshot`
- `ElementNotInteractableException` handler in `element.click`
-### Changed
+#### Changed
- `element.get_rect` output value sorting
- `PlayDriver`/`CoreDriver` class variables moved to `DriverWrapper`
- `os.environ['visual']` changed to `driver_wrapper.visual_regression_path`
- `element.wait_element` exception message
- Mobile: Finding elements in native context now skips parent
-### Fixed
+#### Fixed
- `autolog` params
- `driver_wrapper.switch_to_tab` with default params
---
-## v1.2.6/7
+### v1.2.6/7
*Release date: 2022-09-15*
-### Fixed
+#### Fixed
- screenshot name generation
---
-## v1.2.5
+### v1.2.5
*Release date: 2022-09-13*
-### Added
+#### Added
- `element.click_into_center` method
- `driver_wrapper.click_by_coordinates` method
-### Fixed
+#### Fixed
- `calculate_coordinate_to_click` calculation
- Shared object of groups become unique for each class
---
-## v1.2.4
+### v1.2.4
*Release date: 2022-09-08*
-### Added
+#### Added
- `assert_screenshot()` elements removal
---
-## v1.2.3
+### v1.2.3
*Release date: 2022-09-02*
-### Fixed
+#### Fixed
- `element.is_displayed()` exception handler
---
-## v1.2.1/2
+### v1.2.1/2
*Release date: 2022-08-31*
-### Fixed
+#### Fixed
- Annotations
---
-## v1.2.0
+### v1.2.0
*Release date: 2022-08-31*
-### Added
+#### Added
- [Allure Screen Diff Plugin](https://github.com/allure-framework/allure2/blob/master/plugins/screen-diff-plugin/README.md) support
- Driver specific logs
- Custom exceptions
@@ -712,7 +722,7 @@ previously such elements did not receive `parent` argument
- `KeyboardKeys` class
- `element.send_keyboard_action` method
-### Changed
+#### Changed
- `get_text` property become `text`
- `get_value` property become `value`
- `get_screenshot_base` property become `screenshot_base`
@@ -720,16 +730,16 @@ previously such elements did not receive `parent` argument
- `by_attr` arg of `Checkbox` removed
- `calculate_coordinate_to_click` now calculate coordinates from element location
-### Fixed
+#### Fixed
- Reduced count of `find_element` execution
- Page `driver_wrapper` getter exception
---
-## v1.1.1
+### v1.1.1
*Release date: 2022-08-10*
-### Added
+#### Added
- iOS SafariDriver basic support
- Different second driver support (for mobile/desktop safari)
- Tabs manipulating methods for desktop in `CoreDriver/PlayDriver`
@@ -742,23 +752,23 @@ previously such elements did not receive `parent` argument
- `page.swipe(_up/_down)` methods for mobile
- Default cookie path/domain in `driver_wrapper.set_cookie` method
-### Changed
+#### Changed
- `Driver` becomes `DriverWrapper` for more readability
- Mixins classes renamed and moved to `dyatel.mixins` folder
- Selenium `core_element.wait_element` now using `is_displayed`
- Selenium exception stacktrace reduced in most cases
-### Fixed
+#### Fixed
- Custom `driver_wrapper`/`driver` for child elements
- Selenium `KeyError` of `driver_wrapper.set_cookie` without `domain`
- Driver creation with function scope of pytest
---
-## v1.1.0
+### v1.1.0
*Release date: 2022-07-23*
-### Added
+#### Added
- `Checkbox` class for Playwright and Selenium
- `set_text` method in `Element` class
- `wait_elements_count` method in `Element` class
@@ -766,35 +776,35 @@ previously such elements did not receive `parent` argument
- `wait_element_value` method in `Element` class
- `driver_wrapper` arg for `Group` and `Page`
-### Changed
+#### Changed
- Page/Group `set_driver` workflow
- `CorePage` and `PlayPage` methods moved to `Page`
---
-## v1.0.5
+### v1.0.5
*Release date: 2022-07-10*
-### Added
+#### Added
- `_first_element` property in `PlayElement`
-### Changed
+#### Changed
- `element` property replaced with `_first_element` for elements interactions
- `parent` nesting of `Element` changed from one level to endless
- `PlayElement` / `CoreElement` initialization
-### Fixed
+#### Fixed
- `all_elements` execution time/nesting
---
-## v1.0.4
+### v1.0.4
*Release date: 2022-07-07*
-### Added
+#### Added
- `set_driver` function for page object
- Multiple drivers support
-### Changed
+#### Changed
- Drivers initialization
- `driver`, `driver_wrapper` become property methods
diff --git a/README.md b/README.md
index 173b85d5..fa1586c5 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/mops/__init__.py b/mops/__init__.py
index 5b2143f1..2a466ee6 100644
--- a/mops/__init__.py
+++ b/mops/__init__.py
@@ -1,4 +1,4 @@
"""Wrapper of Selenium, Appium and Playwright with a single API."""
-__version__ = '3.5.2'
+__version__ = '3.5.3'
__project_name__ = 'mops'
diff --git a/tests/static_tests/performance/test_overall_performance.py b/tests/static_tests/performance/test_overall_performance.py
index 15928cf7..3ca268b9 100644
--- a/tests/static_tests/performance/test_overall_performance.py
+++ b/tests/static_tests/performance/test_overall_performance.py
@@ -76,26 +76,14 @@ def test_performance_element_initialisation(mocked_selenium_driver, case, set_el
print('cpu_time=', cpu_time)
print('init_without_profiling_stop_timestamp=', init_without_profiling_stop_timestamp)
- expected_peak_mem = 4.7
- expected_init_duration = 0.4
- init_without_profiling_expected = 0.1
-
- if sys.version_info >= (3, 9):
- expected_peak_mem = 4.7
- expected_init_duration = 0.4
- init_without_profiling_expected = 0.13
- if sys.version_info >= (3, 10):
- expected_peak_mem = 4.6
- expected_init_duration = 0.4
+ expected_peak_mem = 4.8
+ expected_init_duration = 0.55
+ init_without_profiling_expected = 0.20
+
if sys.version_info >= (3, 11):
- expected_peak_mem = 4.0
- expected_init_duration = 0.4
+ expected_peak_mem = 4.8
if sys.version_info >= (3, 12):
- expected_peak_mem = 4.1
- expected_init_duration = 0.4
- if sys.version_info >= (3, 13):
- expected_peak_mem = 4.2
- expected_init_duration = 0.4
+ expected_peak_mem = 5.0
assert init_without_profiling_stop_timestamp < init_without_profiling_expected,\
f'Execution without profiling takes too much time: {init_without_profiling_stop_timestamp}'
@@ -151,29 +139,15 @@ def test_performance_group_initialisation(mocked_selenium_driver, case, set_grou
print('cpu_time=', cpu_time)
print('init_without_profiling_stop_timestamp=', init_without_profiling_stop_timestamp)
- expected_peak_mem = 3.2
- expected_init_duration = 0.4
-
- if sys.version_info >= (3, 9):
- expected_peak_mem = 3.5
- expected_init_duration = 0.4
-
- if sys.version_info >= (3, 10):
- expected_peak_mem = 3.3
- expected_init_duration = 0.4
+ expected_peak_mem = 3.5
+ expected_init_duration = 0.55
if sys.version_info >= (3, 11):
- expected_peak_mem = 2.6
- expected_init_duration = 0.4
-
+ expected_peak_mem = 3.5
if sys.version_info >= (3, 12):
- expected_peak_mem = 2.5
- expected_init_duration = 0.4
- if sys.version_info >= (3, 13):
- expected_peak_mem = 2.8
- expected_init_duration = 0.4
+ expected_peak_mem = 3.5
- assert init_without_profiling_stop_timestamp < 0.15,\
+ assert init_without_profiling_stop_timestamp < 0.20,\
f'Execution without profiling takes too much time: {init_without_profiling_stop_timestamp}'
assert stats.total_tt < expected_init_duration, \
f"Execution time too high: {stats.total_tt:.3f} sec"