From d8db6949be717d3a69ee2d86b7ba34dd303d2381 Mon Sep 17 00:00:00 2001 From: VladimirPodolian Date: Thu, 24 Jul 2025 21:44:35 +0200 Subject: [PATCH 1/7] com.android locator detection fix --- mops/utils/selector_synchronizer.py | 78 ++++++++++++------- .../unit/test_selector_synchronizer.py | 29 ++++++- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/mops/utils/selector_synchronizer.py b/mops/utils/selector_synchronizer.py index 0e668532..adc4edcf 100644 --- a/mops/utils/selector_synchronizer.py +++ b/mops/utils/selector_synchronizer.py @@ -11,11 +11,26 @@ from mops.utils.internal_utils import all_tags -DEFAULT_MATCH = (f"{LocatorType.XPATH}=", f"{LocatorType.ID}=", f"{LocatorType.CSS}=", f"{LocatorType.TEXT}=") -XPATH_MATCH = ("/", "./", "(/") -CSS_MATCH = ("#", ".") -CSS_REGEXP = r"[#.\[\]=]" -APPIUM_LOCATOR_TYPES = ( +_XPATH_MATCH = ("/", "./", "(/") +_CSS_MATCH = ("#", ".") +_CSS_REGEXP = r"[#.\[\]=]" + +_DEFAULT_MATCH = ( + f"{LocatorType.XPATH}=", + f"{LocatorType.ID}=", + f"{LocatorType.CSS}=", + f"{LocatorType.TEXT}=", +) + +_APPIUM_MATCH = ( + f"{LocatorType.XPATH}=", + f"{LocatorType.ID}=", + f"[{LocatorType.ID}=", + f"{LocatorType.CSS}=", + f"{LocatorType.TEXT}=" +) + +_APPIUM_LOCATOR_TYPES = ( f'{LocatorType.IOS_PREDICATE}=', f'{LocatorType.IOS_UIAUTOMATION}=', f'{LocatorType.IOS_CLASS_CHAIN}=', @@ -29,6 +44,19 @@ f'{LocatorType.CUSTOM}=', ) +_SELENIUM_MOPS_LOCATOR_TYPES = { + By.ID: LocatorType.ID, + By.XPATH: LocatorType.XPATH, + By.CSS_SELECTOR: LocatorType.CSS, +} + + +def _set_selenium_compatibility_id_locator(obj: Any, split: bool = True) -> Any: + locator = obj.locator.split(f"{LocatorType.ID}=")[-1] if split else obj.locator + + obj.locator = f'[{LocatorType.ID}="{locator}"]' + obj.locator_type = By.CSS_SELECTOR + def get_platform_locator(obj: Any): """ @@ -66,7 +94,6 @@ def set_selenium_selector(obj: Any): Sets selenium locator & locator type """ locator = obj.locator.strip() - obj.log_locator = locator # Checking the supported locators @@ -84,52 +111,48 @@ def set_selenium_selector(obj: Any): obj.locator_type = By.CSS_SELECTOR elif locator.startswith(f"{LocatorType.ID}="): - locator = obj.locator.split(f"{LocatorType.ID}=")[-1] - obj.locator = f'[{LocatorType.ID}="{locator}"]' - obj.locator_type = By.CSS_SELECTOR + _set_selenium_compatibility_id_locator(obj) # Checking the regular locators - elif locator.startswith(XPATH_MATCH): + elif locator.startswith(_XPATH_MATCH): obj.locator_type = By.XPATH - obj.log_locator = f'{LocatorType.XPATH}={locator}' - elif locator.startswith(CSS_MATCH) or re.search(CSS_REGEXP, locator): + elif locator.startswith(_CSS_MATCH) or re.search(_CSS_REGEXP, locator): obj.locator_type = By.CSS_SELECTOR - obj.log_locator = f'{LocatorType.CSS}={locator}' elif locator in all_tags or all(tag in all_tags for tag in locator.split()): obj.locator_type = By.CSS_SELECTOR - obj.log_locator = f'{LocatorType.CSS}={locator}' # Default to ID if nothing else matches else: - locator = obj.locator.split(f"{LocatorType.ID}=")[-1] - obj.locator = f'[{LocatorType.ID}="{locator}"]' - obj.locator_type = By.CSS_SELECTOR - obj.log_locator = f'{LocatorType.ID}={locator}' + _set_selenium_compatibility_id_locator(obj, split=False) + + mops_locator_type = _SELENIUM_MOPS_LOCATOR_TYPES[obj.locator_type] + obj.log_locator = f'{mops_locator_type}={obj.locator}' def set_playwright_locator(obj: Any): """ Sets playwright locator & locator type """ - locator = obj.locator.strip() + locator: str = obj.locator.strip() + obj.log_locator = locator # Checking the supported locators - if locator.startswith(DEFAULT_MATCH): + if locator.startswith(_DEFAULT_MATCH): obj.locator_type = locator.partition('=')[0] return # Checking the regular locators - elif locator.startswith(XPATH_MATCH): + elif locator.startswith(_XPATH_MATCH): obj.locator_type = LocatorType.XPATH - elif locator.startswith(CSS_MATCH) or re.search(CSS_REGEXP, locator): + elif locator.startswith(_CSS_MATCH) or re.search(_CSS_REGEXP, locator): obj.locator_type = LocatorType.CSS elif locator in all_tags or all(tag in all_tags for tag in locator.split()): @@ -150,12 +173,13 @@ def set_appium_selector(obj: Any): """ set_selenium_selector(obj) - locator = obj.locator.strip() + locator: str = obj.locator.strip() - if ':id/' in locator: # Mobile com.android selector - obj.locator_type = By.CSS_SELECTOR - obj.log_locator = f'{LocatorType.ID}={locator}' - elif locator.startswith(APPIUM_LOCATOR_TYPES): + # Mobile com.android selector + if ':id/' in locator and not locator.startswith(_APPIUM_MATCH): + _set_selenium_compatibility_id_locator(obj) + obj.log_locator = f'{LocatorType.CSS}={obj.locator}' + elif locator.startswith(_APPIUM_LOCATOR_TYPES): partition = locator.partition('=') obj.locator_type = partition[0] obj.locator = partition[-1] diff --git a/tests/static_tests/unit/test_selector_synchronizer.py b/tests/static_tests/unit/test_selector_synchronizer.py index 55ac8a01..ee5d6e13 100644 --- a/tests/static_tests/unit/test_selector_synchronizer.py +++ b/tests/static_tests/unit/test_selector_synchronizer.py @@ -12,9 +12,9 @@ "locator_input, expected_locator, expected_locator_type, expected_log_locator", [ ("xpath=//div", "//div", LocatorType.XPATH, "xpath=//div"), - ("text=Hello", '//*[contains(text(), "Hello")]', LocatorType.XPATH, "text=Hello"), + ("text=Hello", '//*[contains(text(), "Hello")]', LocatorType.XPATH, 'xpath=//*[contains(text(), "Hello")]'), ("css=.class", ".class", By.CSS_SELECTOR, "css=.class"), - ("id=my_id", '[id="my_id"]', By.CSS_SELECTOR, "id=my_id"), + ("id=my_id", '[id="my_id"]', By.CSS_SELECTOR, 'css=[id="my_id"]'), ("/html/body/div", "/html/body/div", LocatorType.XPATH, "xpath=/html/body/div"), ("#my_element", "#my_element", By.CSS_SELECTOR, "css=#my_element"), ("button", "button", By.CSS_SELECTOR, "css=button"), @@ -55,6 +55,9 @@ def test_set_playwright_locator(locator_input, expected_locator): assert expected_locator.partition('=')[0] == mock_obj.locator_type +com_android_locator = 'com.android.settings:id/title' + + @pytest.mark.parametrize( "locator, locator_type", [ @@ -71,11 +74,29 @@ def test_set_playwright_locator(locator_input, expected_locator): ('{"selector"="myCustomSelector", "strategy"="myCustomStrategy"}', '-custom'), ], ) -def test_set_appium_selector(locator, locator_type): +def test_set_appium_native_selector(locator, locator_type): mock_obj = SimpleNamespace() log_locator = f'{locator_type}={locator}' - mock_obj.locator =log_locator + mock_obj.locator = log_locator set_appium_selector(mock_obj) assert locator == mock_obj.locator assert locator_type == mock_obj.locator_type assert log_locator == mock_obj.log_locator + + +@pytest.mark.parametrize( + "locator, source_locator_type, expected_locator, expected_log_locator", + [ + (f'id={com_android_locator}', By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'css=[id="{com_android_locator}"]'), + (com_android_locator, By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'css=[id="{com_android_locator}"]'), + ], +) +def test_set_automatically_appium_selector(locator, source_locator_type, expected_locator, expected_log_locator): + mock_obj = SimpleNamespace() + mock_obj.locator = locator + + set_appium_selector(mock_obj) + + assert expected_locator == mock_obj.locator + assert source_locator_type == mock_obj.locator_type + assert expected_log_locator == mock_obj.log_locator From 119972123c0f4e7bc2f790f7b2192ef7466cf412 Mon Sep 17 00:00:00 2001 From: VladimirPodolian Date: Thu, 24 Jul 2025 22:08:13 +0200 Subject: [PATCH 2/7] Rollback some changes --- mops/utils/selector_synchronizer.py | 9 +++++---- tests/static_tests/unit/test_selector_synchronizer.py | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/mops/utils/selector_synchronizer.py b/mops/utils/selector_synchronizer.py index adc4edcf..0cc9d17f 100644 --- a/mops/utils/selector_synchronizer.py +++ b/mops/utils/selector_synchronizer.py @@ -56,6 +56,7 @@ def _set_selenium_compatibility_id_locator(obj: Any, split: bool = True) -> Any: obj.locator = f'[{LocatorType.ID}="{locator}"]' obj.locator_type = By.CSS_SELECTOR + obj.log_locator = f'{LocatorType.ID}={locator}' def get_platform_locator(obj: Any): @@ -94,6 +95,7 @@ def set_selenium_selector(obj: Any): Sets selenium locator & locator type """ locator = obj.locator.strip() + obj.log_locator = locator # Checking the supported locators @@ -117,21 +119,21 @@ def set_selenium_selector(obj: Any): elif locator.startswith(_XPATH_MATCH): obj.locator_type = By.XPATH + obj.log_locator = f'{LocatorType.XPATH}={locator}' elif locator.startswith(_CSS_MATCH) or re.search(_CSS_REGEXP, locator): obj.locator_type = By.CSS_SELECTOR + obj.log_locator = f'{LocatorType.CSS}={locator}' elif locator in all_tags or all(tag in all_tags for tag in locator.split()): obj.locator_type = By.CSS_SELECTOR + obj.log_locator = f'{LocatorType.CSS}={locator}' # Default to ID if nothing else matches else: _set_selenium_compatibility_id_locator(obj, split=False) - mops_locator_type = _SELENIUM_MOPS_LOCATOR_TYPES[obj.locator_type] - obj.log_locator = f'{mops_locator_type}={obj.locator}' - def set_playwright_locator(obj: Any): """ @@ -178,7 +180,6 @@ def set_appium_selector(obj: Any): # Mobile com.android selector if ':id/' in locator and not locator.startswith(_APPIUM_MATCH): _set_selenium_compatibility_id_locator(obj) - obj.log_locator = f'{LocatorType.CSS}={obj.locator}' elif locator.startswith(_APPIUM_LOCATOR_TYPES): partition = locator.partition('=') obj.locator_type = partition[0] diff --git a/tests/static_tests/unit/test_selector_synchronizer.py b/tests/static_tests/unit/test_selector_synchronizer.py index ee5d6e13..0fc2eb22 100644 --- a/tests/static_tests/unit/test_selector_synchronizer.py +++ b/tests/static_tests/unit/test_selector_synchronizer.py @@ -12,9 +12,10 @@ "locator_input, expected_locator, expected_locator_type, expected_log_locator", [ ("xpath=//div", "//div", LocatorType.XPATH, "xpath=//div"), - ("text=Hello", '//*[contains(text(), "Hello")]', LocatorType.XPATH, 'xpath=//*[contains(text(), "Hello")]'), + ("text=Hello", '//*[contains(text(), "Hello")]', LocatorType.XPATH, 'text=Hello'), ("css=.class", ".class", By.CSS_SELECTOR, "css=.class"), - ("id=my_id", '[id="my_id"]', By.CSS_SELECTOR, 'css=[id="my_id"]'), + ("id=my_id", '[id="my_id"]', By.CSS_SELECTOR, 'id=my_id'), + ("todefault", '[id="todefault"]', By.CSS_SELECTOR, 'id=todefault'), ("/html/body/div", "/html/body/div", LocatorType.XPATH, "xpath=/html/body/div"), ("#my_element", "#my_element", By.CSS_SELECTOR, "css=#my_element"), ("button", "button", By.CSS_SELECTOR, "css=button"), @@ -39,6 +40,7 @@ def test_set_selenium_selector(locator_input, expected_locator, expected_locator ("text=Hello", "text=Hello"), ("css=.class", "css=.class"), ("id=my_id", "id=my_id"), + ("todefault", 'id=todefault'), ("/html/body/div", "xpath=/html/body/div"), ("#my_element", "css=#my_element"), ("button", "css=button"), @@ -87,8 +89,8 @@ def test_set_appium_native_selector(locator, locator_type): @pytest.mark.parametrize( "locator, source_locator_type, expected_locator, expected_log_locator", [ - (f'id={com_android_locator}', By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'css=[id="{com_android_locator}"]'), - (com_android_locator, By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'css=[id="{com_android_locator}"]'), + (f'id={com_android_locator}', By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'id={com_android_locator}'), + (com_android_locator, By.CSS_SELECTOR, f'[id="{com_android_locator}"]', f'id={com_android_locator}'), ], ) def test_set_automatically_appium_selector(locator, source_locator_type, expected_locator, expected_log_locator): From 18d68a16fdbb4eff91bd65405b5446bf1fc68779 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Wed, 29 Oct 2025 12:50:47 +0100 Subject: [PATCH 3/7] 3.2.2: retry decorator on ActionChains methods & DriverWrapper.wait reason arg --- mops/abstraction/driver_wrapper_abc.py | 5 ++++- mops/playwright/play_driver.py | 8 +++++++- mops/selenium/core/core_driver.py | 8 +++++++- mops/selenium/elements/web_element.py | 8 +++++++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mops/abstraction/driver_wrapper_abc.py b/mops/abstraction/driver_wrapper_abc.py index b6c8b3b6..fde0032a 100644 --- a/mops/abstraction/driver_wrapper_abc.py +++ b/mops/abstraction/driver_wrapper_abc.py @@ -95,13 +95,16 @@ def quit(self, silent: bool = False, trace_path: str = 'trace.zip'): """ raise NotImplementedError() - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> DriverWrapper: + def wait(self, timeout: Union[int, float] = WAIT_UNIT, reason: str = '') -> DriverWrapper: """ Pauses the execution for a specified amount of time. :param timeout: The time to sleep in seconds (can be an integer or float). :type timeout: typing.Union[int, float] + :param reason: The waiting reason. + :type reason: str + :return: :obj:`.DriverWrapper` - The current instance of the driver wrapper. """ raise NotImplementedError() diff --git a/mops/playwright/play_driver.py b/mops/playwright/play_driver.py index fee54f6b..65bf5077 100644 --- a/mops/playwright/play_driver.py +++ b/mops/playwright/play_driver.py @@ -66,15 +66,21 @@ def is_firefox(self) -> bool: """ return self.browser_name.lower() == 'firefox' - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> PlayDriver: + def wait(self, timeout: Union[int, float] = WAIT_UNIT, reason: str = '') -> PlayDriver: """ Pauses the execution for a specified amount of time. :param timeout: The time to sleep in seconds (can be an integer or float). :type timeout: typing.Union[int, float] + :param reason: The waiting reason. + :type reason: str + :return: :obj:`.PlayDriver` - The current instance of the driver wrapper. """ + if reason: + self.log(reason) + self.driver.wait_for_timeout(get_timeout_in_ms(timeout)) return self diff --git a/mops/selenium/core/core_driver.py b/mops/selenium/core/core_driver.py index 99513f7c..de832906 100644 --- a/mops/selenium/core/core_driver.py +++ b/mops/selenium/core/core_driver.py @@ -89,15 +89,21 @@ def get_window_size(self) -> Size: """ return Size(**self.driver.get_window_size()) - def wait(self, timeout: Union[int, float] = WAIT_UNIT) -> CoreDriver: + def wait(self, timeout: Union[int, float] = WAIT_UNIT, reason: str = '') -> CoreDriver: """ Pauses the execution for a specified amount of time. :param timeout: The time to sleep in seconds (can be an integer or float). :type timeout: typing.Union[int, float] + :param reason: The waiting reason. + :type reason: str + :return: :obj:`.CoreDriver` - The current instance of the driver wrapper. """ + if reason: + self.log(reason) + time.sleep(timeout) return self diff --git a/mops/selenium/elements/web_element.py b/mops/selenium/elements/web_element.py index 5ab487a7..833da7e4 100644 --- a/mops/selenium/elements/web_element.py +++ b/mops/selenium/elements/web_element.py @@ -2,8 +2,11 @@ from abc import ABC +from selenium.common import JavascriptException + from mops.selenium.core.core_element import CoreElement from mops.js_scripts import js_click +from mops.utils.decorators import retry from mops.utils.internal_utils import calculate_coordinate_to_click from mops.utils.selector_synchronizer import get_platform_locator, set_selenium_selector @@ -44,7 +47,7 @@ def click(self, *, force_wait: bool = True, **kwargs) -> WebElement: return self - + @retry(JavascriptException) def hover(self, silent: bool = False) -> WebElement: """ Hover the mouse over the current element. @@ -63,6 +66,7 @@ def hover(self, silent: bool = False) -> WebElement: .perform() return self + @retry(JavascriptException) def hover_outside(self, x: int = 0, y: int = -5) -> WebElement: """ Hover the mouse outside the current element, by default 5px above it. @@ -84,6 +88,7 @@ def hover_outside(self, x: int = 0, y: int = -5) -> WebElement: .perform() return self + @retry(JavascriptException) def click_outside(self, x: int = -5, y: int = -5) -> WebElement: """ Perform a click outside the current element, by default 5px left and above it. @@ -104,6 +109,7 @@ def click_outside(self, x: int = -5, y: int = -5) -> WebElement: self.driver_wrapper.click_by_coordinates(x=x, y=y, silent=True) return self + @retry(JavascriptException) def click_into_center(self, silent: bool = False) -> WebElement: """ Clicks at the center of the element. From 4498e62b73620fac0eeaafc34b8cdf2b6f603761 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sun, 9 Nov 2025 16:18:15 +0100 Subject: [PATCH 4/7] Changes: PlayElement.scroll_into_view removed PlayElement.click replaced with `tap` on mobile resolution CoreElement.scroll_into_view moved to Element DriverWrapper.get_scroll_position added --- mops/abstraction/driver_wrapper_abc.py | 6 ++-- mops/abstraction/element_abc.py | 6 ++-- mops/base/driver_wrapper.py | 8 +++++ mops/base/element.py | 45 ++++++++++++++++++++++++-- mops/playwright/play_driver.py | 6 ++-- mops/playwright/play_element.py | 44 ++++--------------------- mops/selenium/core/core_driver.py | 6 ++-- mops/selenium/core/core_element.py | 33 ------------------- tests/mobile_tests/test_swipe.py | 8 ++--- 9 files changed, 71 insertions(+), 91 deletions(-) diff --git a/mops/abstraction/driver_wrapper_abc.py b/mops/abstraction/driver_wrapper_abc.py index fde0032a..2a19ebb3 100644 --- a/mops/abstraction/driver_wrapper_abc.py +++ b/mops/abstraction/driver_wrapper_abc.py @@ -225,15 +225,15 @@ def switch_to_default_content(self) -> DriverWrapper: """ raise NotImplementedError() - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ Synchronously executes JavaScript in the current window or frame. Compatible with Selenium's `execute_script` method. :param script: The JavaScript code to execute. :type script: str - :param args: Any arguments to pass to the JavaScript (e.g., Element object). - :type args: list + :param args: Any arguments to pass to the JavaScript. + :type args: :obj:`typing.Any` :return: :obj:`typing.Any` - The result of the JavaScript execution. """ raise NotImplementedError() diff --git a/mops/abstraction/element_abc.py b/mops/abstraction/element_abc.py index cf4b13f1..aeaf4308 100644 --- a/mops/abstraction/element_abc.py +++ b/mops/abstraction/element_abc.py @@ -266,14 +266,14 @@ def hide(self) -> Element: """ raise NotImplementedError() - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ Executes 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: Additional arguments for the script, - that appear in script as ``arguments[1]`` ``arguments[2]`` etc. + :param args: Any arguments to pass to the JavaScript. + :type args: :obj:`typing.Any` :return: :obj:`typing.Any` result from the script. """ raise NotImplementedError() diff --git a/mops/base/driver_wrapper.py b/mops/base/driver_wrapper.py index 8b8c8d38..ff6dc9e8 100644 --- a/mops/base/driver_wrapper.py +++ b/mops/base/driver_wrapper.py @@ -220,6 +220,14 @@ def save_screenshot( return image_object + def get_scroll_position(self) -> int: + """ + Returns the current vertical scroll position of the page. + + :return: :class:`int` - Current vertical scroll offset in pixels. + """ + return self.execute_script('return window.pageYOffset') + def assert_screenshot( self, filename: str = '', diff --git a/mops/base/element.py b/mops/base/element.py index ceacb8a9..2c36f7db 100644 --- a/mops/base/element.py +++ b/mops/base/element.py @@ -1,10 +1,12 @@ from __future__ import annotations +import time from copy import copy from typing import Union, List, Type, Tuple, Optional, TYPE_CHECKING from PIL.Image import Image +from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes, scroll_into_view_blocks from mops.mixins.objects.wait_result import Result from playwright.sync_api import Page as PlaywrightDriver from appium.webdriver.webdriver import WebDriver as AppiumDriver @@ -696,6 +698,43 @@ def is_fully_visible(self, check_displaying: bool = True, silent: bool = False) return is_visible + def scroll_into_view( + self, + block: ScrollTo = ScrollTo.CENTER, + behavior: ScrollTypes = ScrollTypes.INSTANT, + sleep: Union[int, float] = 0, + silent: bool = False, + ) -> Element: + """ + Scrolls the element into view using a JavaScript script. + + :param block: The scrolling block alignment. One of the :class:`.ScrollTo` options. + :type block: ScrollTo + :param behavior: The scrolling behavior. One of the :class:`.ScrollTypes` options. + :type behavior: ScrollTypes + :param sleep: Delay in seconds after scrolling. Can be an integer or a float. + :type sleep: typing.Union[int, float] + :param silent: If :obj:`True`, suppresses logging. + :type silent: bool + :return: :class:`Element` + """ + if not silent: + self.log(f'Scroll element "{self.name}" into view') + + if block not in scroll_into_view_blocks: + message = f'Provide one of {scroll_into_view_blocks} option in `block` argument' + raise UnsuitableArgumentsException(message) + + self.execute_script( + 'arguments[0].scrollIntoView({block: arguments[1], behavior: arguments[2]});', + block, behavior + ) + + if sleep: + time.sleep(sleep) + + return self + def save_screenshot( self, file_name: str, @@ -735,14 +774,14 @@ def hide(self) -> Element: self.execute_script('arguments[0].style.opacity = "0";') return self - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ Executes a JavaScript script on the element. :param script: JavaScript code to be executed, referring to the element as ``arguments[0]``. :type script: str - :param args: Additional arguments for the script, - that appear in script as ``arguments[1]`` ``arguments[2]`` etc. + :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, *[arg for arg in args]]) diff --git a/mops/playwright/play_driver.py b/mops/playwright/play_driver.py index 65bf5077..093d799c 100644 --- a/mops/playwright/play_driver.py +++ b/mops/playwright/play_driver.py @@ -251,15 +251,15 @@ def switch_to_default_content(self) -> PlayDriver: self.driver = self._base_driver return self - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ Synchronously executes JavaScript in the current window or frame. Compatible with Selenium's `execute_script` method. :param script: The JavaScript code to execute. :type script: str - :param args: Any arguments to pass to the JavaScript (e.g., Element object). - :type args: list + :param args: Any arguments to pass to the JavaScript. + :type args: :obj:`typing.Any` :return: :obj:`typing.Any` - The result of the JavaScript execution. """ script = script.replace('return ', '') diff --git a/mops/playwright/play_element.py b/mops/playwright/play_element.py index 0f3aea42..1ab968b7 100644 --- a/mops/playwright/play_element.py +++ b/mops/playwright/play_element.py @@ -1,13 +1,11 @@ from __future__ import annotations -import time from abc import ABC from typing import Union, List, Any from PIL.Image import Image from mops.keyboard_keys import KeyboardKeys -from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes -from playwright.sync_api import TimeoutError as PlayTimeoutError, Error +from playwright.sync_api import Error from playwright.sync_api import Page as PlaywrightPage from playwright.sync_api import Locator, Page, Browser, BrowserContext @@ -16,12 +14,10 @@ from mops.utils.decorators import retry from mops.utils.selector_synchronizer import get_platform_locator, set_playwright_locator from mops.abstraction.element_abc import ElementABC -from mops.exceptions import TimeoutException, InvalidSelectorException +from mops.exceptions import InvalidSelectorException from mops.utils.logs import Logging from mops.shared_utils import cut_log_data, get_image from mops.utils.internal_utils import ( - WAIT_EL, - get_timeout_in_ms, calculate_coordinate_to_click, is_group, is_element, @@ -105,7 +101,11 @@ def click(self, *, force_wait: bool = True, **kwargs) -> PlayElement: if force_wait: self.wait_visibility(silent=True) - self._first_element.click(**kwargs) + if self.driver_wrapper.is_mobile_resolution: + self._first_element.tap(**kwargs) + else: + self._first_element.click(**kwargs) + return self def click_outside(self, x: int = -5, y: int = -5) -> PlayElement: @@ -244,36 +244,6 @@ def uncheck(self) -> PlayElement: # Element state - def scroll_into_view( - self, - block: ScrollTo = ScrollTo.CENTER, - behavior: ScrollTypes = ScrollTypes.INSTANT, - sleep: Union[int, float] = 0, - silent: bool = False, - ) -> PlayElement: - """ - Scrolls the element into view using a JavaScript script. - - :param block: The scrolling block alignment. One of the :class:`.ScrollTo` options. - :type block: ScrollTo - :param behavior: The scrolling behavior. One of the :class:`.ScrollTypes` options. - :type behavior: ScrollTypes - :param sleep: Delay in seconds after scrolling. Can be an integer or a float. - :type sleep: typing.Union[int, float] - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`PlayElement` - """ - if not silent: - self.log(f'Scroll element "{self.name}" into view') - - self._first_element.scroll_into_view_if_needed() - - if sleep: - time.sleep(sleep) - - return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. diff --git a/mops/selenium/core/core_driver.py b/mops/selenium/core/core_driver.py index de832906..32a772d8 100644 --- a/mops/selenium/core/core_driver.py +++ b/mops/selenium/core/core_driver.py @@ -291,15 +291,15 @@ def switch_to_default_content(self) -> CoreDriver: self.driver.switch_to.default_content() return self - def execute_script(self, script: str, *args) -> Any: + def execute_script(self, script: str, *args: Any) -> Any: """ Synchronously executes JavaScript in the current window or frame. Compatible with Selenium's `execute_script` method. :param script: The JavaScript code to execute. :type script: str - :param args: Any arguments to pass to the JavaScript (e.g., Element object). - :type args: list + :param args: Any arguments to pass to the JavaScript. + :type args: :obj:`typing.Any` :return: :obj:`typing.Any` - The result of the JavaScript execution. """ args = [getattr(arg, 'element', arg) for arg in args] diff --git a/mops/selenium/core/core_element.py b/mops/selenium/core/core_element.py index 9cecce9f..40ffb44a 100644 --- a/mops/selenium/core/core_element.py +++ b/mops/selenium/core/core_element.py @@ -23,7 +23,6 @@ from mops.js_scripts import get_element_size_js, get_element_position_on_screen_js, hide_caret_js_script from mops.keyboard_keys import KeyboardKeys from mops.mixins.objects.location import Location -from mops.mixins.objects.scrolls import ScrollTo, ScrollTypes, scroll_into_view_blocks from mops.mixins.objects.size import Size from mops.shared_utils import cut_log_data, _scaled_screenshot from mops.utils.internal_utils import WAIT_EL, safe_call, get_dict, is_group @@ -206,38 +205,6 @@ def uncheck(self) -> CoreElement: # Element state - def scroll_into_view( - self, - block: ScrollTo = ScrollTo.CENTER, - behavior: ScrollTypes = ScrollTypes.INSTANT, - sleep: Union[int, float] = 0, - silent: bool = False, - ) -> CoreElement: - """ - Scrolls the element into view using a JavaScript script. - - :param block: The scrolling block alignment. One of the :class:`.ScrollTo` options. - :type block: ScrollTo - :param behavior: The scrolling behavior. One of the :class:`.ScrollTypes` options. - :type behavior: ScrollTypes - :param sleep: Delay in seconds after scrolling. Can be an integer or a float. - :type sleep: typing.Union[int, float] - :param silent: If :obj:`True`, suppresses logging. - :type silent: bool - :return: :class:`CoreElement` - """ - if not silent: - self.log(f'Scroll element "{self.name}" into view') - - assert block in scroll_into_view_blocks, f'Provide one of {scroll_into_view_blocks} option in `block` argument' - - self.execute_script(f'arguments[0].scrollIntoView({{block: "{block}", behavior: "{behavior}"}});') - - if sleep: - time.sleep(sleep) - - return self - def screenshot_image(self, screenshot_base: bytes = None) -> Image: """ Returns a :class:`PIL.Image.Image` object representing the screenshot of the web element. diff --git a/tests/mobile_tests/test_swipe.py b/tests/mobile_tests/test_swipe.py index e17b20a6..d79e8f5b 100644 --- a/tests/mobile_tests/test_swipe.py +++ b/tests/mobile_tests/test_swipe.py @@ -1,12 +1,8 @@ -def get_scroll_position(driver): - return driver.execute_script('return window.pageYOffset || document.documentElement.scrollTop') - - def test_swipe(second_playground_page, driver_wrapper): expected_scroll = range(300, 400) if driver_wrapper.is_android else range(1050, 1200) second_playground_page.swipe(0, 500, 0, 100, sleep=0.3) - scroll = get_scroll_position(driver_wrapper.driver) + scroll = driver_wrapper.get_scroll_position() assert scroll in expected_scroll second_playground_page.swipe(0, 100, 0, 500, sleep=0.3) - scroll = get_scroll_position(driver_wrapper.driver) + scroll = driver_wrapper.get_scroll_position() assert scroll == 0 From 4863e96d26cd163b8a5331f2be688382cd96faa3 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sun, 9 Nov 2025 16:20:12 +0100 Subject: [PATCH 5/7] DriverWrapperABC.get_scroll_position added --- mops/abstraction/driver_wrapper_abc.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mops/abstraction/driver_wrapper_abc.py b/mops/abstraction/driver_wrapper_abc.py index 2a19ebb3..59d433bd 100644 --- a/mops/abstraction/driver_wrapper_abc.py +++ b/mops/abstraction/driver_wrapper_abc.py @@ -310,6 +310,14 @@ def save_screenshot( """ raise NotImplementedError() + def get_scroll_position(self) -> int: + """ + Returns the current vertical scroll position of the page. + + :return: :class:`int` - Current vertical scroll offset in pixels. + """ + raise NotImplementedError() + def assert_screenshot( self, filename: str = '', From 070147f54ae2481c6e78c83ec045bf83a4186f30 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sun, 9 Nov 2025 16:28:05 +0100 Subject: [PATCH 6/7] Tests: Sleep after appium launch --- .github/workflows/appium_android_tests.yml | 1 + .github/workflows/appium_ios_tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/appium_android_tests.yml b/.github/workflows/appium_android_tests.yml index 7ee3c459..d5d65329 100644 --- a/.github/workflows/appium_android_tests.yml +++ b/.github/workflows/appium_android_tests.yml @@ -39,6 +39,7 @@ jobs: - name: Start Appium Server in the Background run: | nohup appium --allow-insecure chromedriver_autodownload > appium.log 2>&1 & + sleep 20 - name: Enable KVM run: | diff --git a/.github/workflows/appium_ios_tests.yml b/.github/workflows/appium_ios_tests.yml index 7ab2354e..c9e90e03 100644 --- a/.github/workflows/appium_ios_tests.yml +++ b/.github/workflows/appium_ios_tests.yml @@ -39,6 +39,7 @@ jobs: - name: Start Appium Server in the Background run: | nohup appium > appium.log 2>&1 & + sleep 20 - name: Run Appium Safari tests id: tests From 5cccc3ce4c230996eac29c846a22917d64418147 Mon Sep 17 00:00:00 2001 From: Vladimir Podolyan Date: Sun, 9 Nov 2025 16:35:09 +0100 Subject: [PATCH 7/7] Tests: Newer ios version specified --- .github/workflows/appium_android_tests.yml | 2 +- .github/workflows/appium_ios_tests.yml | 2 +- tests/settings.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/appium_android_tests.yml b/.github/workflows/appium_android_tests.yml index d5d65329..f974d74b 100644 --- a/.github/workflows/appium_android_tests.yml +++ b/.github/workflows/appium_android_tests.yml @@ -39,7 +39,7 @@ jobs: - name: Start Appium Server in the Background run: | nohup appium --allow-insecure chromedriver_autodownload > appium.log 2>&1 & - sleep 20 + sleep 10 - name: Enable KVM run: | diff --git a/.github/workflows/appium_ios_tests.yml b/.github/workflows/appium_ios_tests.yml index c9e90e03..4720b7b2 100644 --- a/.github/workflows/appium_ios_tests.yml +++ b/.github/workflows/appium_ios_tests.yml @@ -39,7 +39,7 @@ jobs: - name: Start Appium Server in the Background run: | nohup appium > appium.log 2>&1 & - sleep 20 + sleep 10 - name: Run Appium Safari tests id: tests diff --git a/tests/settings.py b/tests/settings.py index 0560a188..dcec2729 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -30,7 +30,7 @@ def get_ios_desired_caps(): return { 'deviceName': env.get('IOS_DEVICE_NAME') or 'iPhone 16', - 'platformVersion': env.get('IOS_PLATFORM_VERSION') or '18.2', + 'platformVersion': env.get('IOS_PLATFORM_VERSION') or '18.6', 'udid': env.get('udid') or '', 'automationName': 'XCUITest', 'platformName': 'iOS',