Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

<br>

## v3.4.3

### 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
- `PlayDriver.get_inner_window_size` null-safe viewport handling (returns `Size(0, 0)` when viewport is `None`)

---

## v3.4.2
*Release date: 2026-03-28*

Expand Down
2 changes: 1 addition & 1 deletion docs/source/driver_wrapper/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ experience for managing driver sessions, performing operations, and handling cro
- The `DriverWrapper` and its underlying `Driver` instance are easily accessible within your `Page`, `Group`, and `Element` objects, allowing for consistent and efficient interactions across your test suite.

3. **Dynamic Status Attributes:**
- `DriverWrapper` provides various status attributes (e.g., `is_mobile`, `is_selenium`, `is_playwright`) that help you tailor your test behavior based on the current driver environment, ensuring more precise control and adaptability in your tests.
- `DriverWrapper` provides various status attributes (e.g., `is_mobile`, `is_selenium`, `is_playwright`, `is_cdp`) that help you tailor your test behavior based on the current driver environment, ensuring more precise control and adaptability in your tests.

4. **Optimal Driver Setup:**
- The initialization of the source driver should be handled within your testing framework. This approach ensures that the browser or device starts with the most appropriate configuration, leading to more reliable and efficient test executions.
Expand Down
2 changes: 1 addition & 1 deletion mops/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = '3.4.2'
__version__ = '3.4.3'
__project_name__ = 'mops'
2 changes: 2 additions & 0 deletions mops/abstraction/driver_wrapper_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class DriverWrapperABC(ABC):
is_simulator: bool = False
is_real_device: bool = False

is_cdp: bool = False

browser_name: Union[str, None] = None

@cached_property
Expand Down
2 changes: 2 additions & 0 deletions mops/base/driver_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ class DriverWrapper(InternalMixin, Logging, DriverWrapperABC):
is_simulator: bool = False
is_real_device: bool = False

is_cdp: bool = False

browser_name: Union[str, None] = None

def __new__(cls, *args, **kwargs):
Expand Down
22 changes: 18 additions & 4 deletions mops/playwright/play_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,25 @@ def quit(self, silent: bool = False, trace_path: str = 'trace.zip'):

:return: :obj:`None`
"""
if trace_path:
if trace_path and not self.is_cdp:
try:
self.context.tracing.stop(path=trace_path)
except PlaywrightError:
pass

self._base_driver.close()
self.context.close()
if self.is_cdp:
try:
self._base_driver.close()
except PlaywrightError:
pass

try:
self.context.close()
except PlaywrightError:
pass
else:
self._base_driver.close()
self.context.close()

def set_cookie(self, cookies: List[dict]) -> PlayDriver:
"""
Expand Down Expand Up @@ -313,7 +324,10 @@ def get_inner_window_size(self) -> Size:

:return: The size of the inner window as a :class:`.Size` object.
"""
return Size(**self.driver.viewport_size)
viewport = self.driver.viewport_size
if viewport is None:
return Size(width=0, height=0)
return Size(**viewport)

def get_window_size(self) -> Size:
"""
Expand Down
8 changes: 7 additions & 1 deletion mops/selenium/core/core_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,13 @@ def quit(self, silent: bool = False, trace_path: str = 'trace.zip'):

:return: :obj:`None`
"""
self.driver.quit()
if self.is_cdp:
try:
self.driver.quit()
except SeleniumWebDriverException:
pass
else:
self.driver.quit()

def set_cookie(self, cookies: List[dict]) -> CoreDriver:
"""
Expand Down
136 changes: 136 additions & 0 deletions tests/static_tests/unit/test_cdp_robustness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import pytest

from mock.mock import MagicMock, PropertyMock, patch

from playwright.sync_api import Page as PlaywrightSourcePage, Browser
from playwright._impl._errors import Error as PlaywrightError

from selenium.common.exceptions import WebDriverException as SeleniumWebDriverException
from selenium.webdriver.remote.webdriver import WebDriver as SeleniumDriver

from mops.base.driver_wrapper import DriverWrapper, DriverWrapperSessions
from mops.mixins.objects.driver import Driver


@pytest.fixture(autouse=True)
def cleanup_sessions():
yield
DriverWrapperSessions.all_sessions = []


def _make_playwright_wrapper(is_cdp=False):
mock_page = PlaywrightSourcePage(MagicMock())
mock_context = MagicMock()
mock_browser = Browser(MagicMock())

wrapper = DriverWrapper(Driver(driver=mock_page, context=mock_context, instance=mock_browser))
wrapper.is_cdp = is_cdp
return wrapper, mock_page, mock_context


def _make_selenium_wrapper(is_cdp=False):
selenium_driver = SeleniumDriver
selenium_driver.__init__ = lambda *args, **kwargs: None
selenium_driver.session_id = None
selenium_driver.command_executor = MagicMock()
selenium_driver.error_handler = MagicMock()
selenium_driver.caps = {}

instance = selenium_driver()
wrapper = DriverWrapper(Driver(driver=instance))
wrapper.is_cdp = is_cdp
return wrapper, instance


class TestIsCdpFlag:

def test_is_cdp_defaults_to_false_playwright(self):
wrapper, _, _ = _make_playwright_wrapper()
assert wrapper.is_cdp is False

def test_is_cdp_defaults_to_false_selenium(self):
wrapper, _ = _make_selenium_wrapper()
assert wrapper.is_cdp is False

def test_is_cdp_can_be_set_true_playwright(self):
wrapper, _, _ = _make_playwright_wrapper(is_cdp=True)
assert wrapper.is_cdp is True

def test_is_cdp_can_be_set_true_selenium(self):
wrapper, _ = _make_selenium_wrapper(is_cdp=True)
assert wrapper.is_cdp is True


class TestPlayDriverCdpQuit:

def test_cdp_quit_suppresses_page_close_error(self):
wrapper, mock_page, mock_context = _make_playwright_wrapper(is_cdp=True)
mock_page.close = MagicMock(side_effect=PlaywrightError('Target page closed'))
mock_context.close = MagicMock()

wrapper.quit(silent=True)
mock_page.close.assert_called_once()

def test_cdp_quit_suppresses_context_close_error(self):
wrapper, mock_page, mock_context = _make_playwright_wrapper(is_cdp=True)
mock_page.close = MagicMock()
mock_context.close = MagicMock(side_effect=PlaywrightError('Context closed'))

wrapper.quit(silent=True)
mock_context.close.assert_called_once()

def test_cdp_quit_skips_tracing(self):
wrapper, _, mock_context = _make_playwright_wrapper(is_cdp=True)
wrapper.quit(silent=True, trace_path='trace.zip')
mock_context.tracing.stop.assert_not_called()

def test_non_cdp_quit_calls_tracing(self):
wrapper, mock_page, mock_context = _make_playwright_wrapper(is_cdp=False)
mock_page.close = MagicMock()
mock_context.close = MagicMock()
wrapper.quit(silent=True, trace_path='trace.zip')
mock_context.tracing.stop.assert_called_once_with(path='trace.zip')

def test_non_cdp_quit_propagates_close_error(self):
wrapper, mock_page, mock_context = _make_playwright_wrapper(is_cdp=False)
mock_page.close = MagicMock(side_effect=PlaywrightError('Unexpected error'))
mock_context.tracing.stop = MagicMock()

with pytest.raises(PlaywrightError):
wrapper.quit(silent=True)


class TestCoreDriverCdpQuit:

def test_cdp_quit_suppresses_webdriver_error(self):
wrapper, driver_instance = _make_selenium_wrapper(is_cdp=True)
driver_instance.quit = MagicMock(side_effect=SeleniumWebDriverException('Browser already closed'))

wrapper.quit(silent=True)
driver_instance.quit.assert_called_once()

def test_non_cdp_quit_propagates_webdriver_error(self):
wrapper, driver_instance = _make_selenium_wrapper(is_cdp=False)
driver_instance.quit = MagicMock(side_effect=SeleniumWebDriverException('Unexpected error'))

with pytest.raises(SeleniumWebDriverException):
wrapper.quit(silent=True)


class TestPlayDriverViewportNullSafe:

def test_get_inner_window_size_returns_zero_when_viewport_none(self):
wrapper, mock_page, _ = _make_playwright_wrapper()
type(mock_page).viewport_size = PropertyMock(return_value=None)

size = wrapper.get_inner_window_size()
assert size.width == 0
assert size.height == 0

def test_get_inner_window_size_returns_values_when_viewport_set(self):
wrapper, mock_page, _ = _make_playwright_wrapper()
type(mock_page).viewport_size = PropertyMock(return_value={'width': 1920, 'height': 1080})

size = wrapper.get_inner_window_size()
assert size.width == 1920
assert size.height == 1080