From 167cfc4fff54882bdf3e7765fe3226a0f2f99549 Mon Sep 17 00:00:00 2001 From: Nacy Dev Date: Fri, 6 Nov 2026 08:35:54 +0700 Subject: [PATCH] fix(typing): refine WebDriver, WebElement, MobileSwitchTo, and AppiumBy type annotations --- .../protocols/webdriver/can_find_elements.py | 15 ++----- appium/webdriver/common/appiumby.py | 39 +++++++++---------- .../flutter_integration/flutter_finder.py | 12 +++--- appium/webdriver/switch_to.py | 10 +++++ appium/webdriver/webdriver.py | 11 +++--- appium/webdriver/webelement.py | 5 +-- test/unit/webdriver/context_test.py | 15 +++++++ 7 files changed, 61 insertions(+), 46 deletions(-) diff --git a/appium/protocols/webdriver/can_find_elements.py b/appium/protocols/webdriver/can_find_elements.py index 9ccc60c0a..c3a7debb7 100644 --- a/appium/protocols/webdriver/can_find_elements.py +++ b/appium/protocols/webdriver/can_find_elements.py @@ -14,9 +14,9 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable -if TYPE_CHECKING: - from selenium.webdriver.common.by import By +from selenium.webdriver.common.by import By +if TYPE_CHECKING: from appium.webdriver.webelement import WebElement @@ -29,13 +29,6 @@ class CanFindElements(Protocol): - find_elements(by, value): Find multiple elements """ - if TYPE_CHECKING: - - def find_element(self, by: str = By.ID, value: str | dict | None = None) -> 'WebElement': ... - - def find_elements(self, by: str = By.ID, value: str | dict | None = None) -> list['WebElement']: ... - else: - - def find_element(self, by: str = 'id', value: str | dict | None = None) -> 'WebElement': ... + def find_element(self, by: str | By = By.ID, value: str | dict | None = None) -> 'WebElement': ... - def find_elements(self, by: str = 'id', value: str | dict | None = None) -> list['WebElement']: ... + def find_elements(self, by: str | By = By.ID, value: str | dict | None = None) -> list['WebElement']: ... diff --git a/appium/webdriver/common/appiumby.py b/appium/webdriver/common/appiumby.py index 371573ab0..b5d695e16 100644 --- a/appium/webdriver/common/appiumby.py +++ b/appium/webdriver/common/appiumby.py @@ -16,26 +16,6 @@ from selenium.webdriver.common.by import By - -class AppiumBy(By): - IOS_PREDICATE = '-ios predicate string' - IOS_CLASS_CHAIN = '-ios class chain' - ANDROID_UIAUTOMATOR = '-android uiautomator' - ANDROID_VIEWTAG = '-android viewtag' - ANDROID_DATA_MATCHER = '-android datamatcher' - ANDROID_VIEW_MATCHER = '-android viewmatcher' - ACCESSIBILITY_ID = 'accessibility id' - IMAGE = '-image' - CUSTOM = '-custom' - - # For Flutter integration usage https://github.com/AppiumTestDistribution/appium-flutter-integration-driver/tree/main - FLUTTER_INTEGRATION_SEMANTICS_LABEL = '-flutter semantics label' - FLUTTER_INTEGRATION_TYPE = '-flutter type' - FLUTTER_INTEGRATION_KEY = '-flutter key' - FLUTTER_INTEGRATION_TEXT = '-flutter text' - FLUTTER_INTEGRATION_TEXT_CONTAINING = '-flutter text containing' - - ByType = Literal[ '-ios predicate string', '-ios class chain', @@ -52,3 +32,22 @@ class AppiumBy(By): '-flutter text', '-flutter text containing', ] + + +class AppiumBy(By): + IOS_PREDICATE: ByType = '-ios predicate string' + IOS_CLASS_CHAIN: ByType = '-ios class chain' + ANDROID_UIAUTOMATOR: ByType = '-android uiautomator' + ANDROID_VIEWTAG: ByType = '-android viewtag' + ANDROID_DATA_MATCHER: ByType = '-android datamatcher' + ANDROID_VIEW_MATCHER: ByType = '-android viewmatcher' + ACCESSIBILITY_ID: ByType = 'accessibility id' + IMAGE: ByType = '-image' + CUSTOM: ByType = '-custom' + + # For Flutter integration usage https://github.com/AppiumTestDistribution/appium-flutter-integration-driver/tree/main + FLUTTER_INTEGRATION_SEMANTICS_LABEL: ByType = '-flutter semantics label' + FLUTTER_INTEGRATION_TYPE: ByType = '-flutter type' + FLUTTER_INTEGRATION_KEY: ByType = '-flutter key' + FLUTTER_INTEGRATION_TEXT: ByType = '-flutter text' + FLUTTER_INTEGRATION_TEXT_CONTAINING: ByType = '-flutter text containing' diff --git a/appium/webdriver/extensions/flutter_integration/flutter_finder.py b/appium/webdriver/extensions/flutter_integration/flutter_finder.py index a92425f27..c58ba8ada 100644 --- a/appium/webdriver/extensions/flutter_integration/flutter_finder.py +++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py @@ -15,8 +15,6 @@ # specific language governing permissions and limitations # under the License. -from typing import cast - from selenium.webdriver.common.by import ByType as SeleniumByType from appium.webdriver.common.appiumby import AppiumBy @@ -30,23 +28,23 @@ def __init__(self, using: SeleniumByType | AppiumByType, value: str) -> None: @staticmethod def by_key(value: str) -> 'FlutterFinder': - return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_KEY), value) + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_KEY, value) @staticmethod def by_text(value: str) -> 'FlutterFinder': - return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TEXT), value) + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TEXT, value) @staticmethod def by_semantics_label(value: str) -> 'FlutterFinder': - return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL), value) + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, value) @staticmethod def by_type(value: str) -> 'FlutterFinder': - return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TYPE), value) + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TYPE, value) @staticmethod def by_text_containing(value: str) -> 'FlutterFinder': - return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING), value) + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, value) def to_dict(self) -> dict: return {'using': self.using, 'value': self.value} diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index f61425247..f30443f19 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -13,13 +13,23 @@ # limitations under the License. +from typing import TYPE_CHECKING + from selenium.webdriver.remote.switch_to import SwitchTo from typing_extensions import Self from .mobilecommand import MobileCommand +if TYPE_CHECKING: + from .webelement import WebElement as MobileWebElement + class MobileSwitchTo(SwitchTo): + if TYPE_CHECKING: + + @property + def active_element(self) -> MobileWebElement: ... + def context(self, context_name: str | None) -> Self: """Sets the context for the current session. Passing `None` is equal to switching to native context. diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 5cd8d52e7..35e4c2700 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -66,6 +66,7 @@ if TYPE_CHECKING: from selenium.webdriver.common.by import By + from selenium.webdriver.support.relative_locator import RelativeBy class ExtensionBase: @@ -291,11 +292,11 @@ def __init__( if TYPE_CHECKING: - def find_element(self, by: str = By.ID, value: str | dict | None = None) -> 'MobileWebElement': # type: ignore[override] - ... + def find_element(self, by: str | By | RelativeBy = By.ID, value: str | dict | None = None) -> 'MobileWebElement': ... - def find_elements(self, by: str = By.ID, value: str | dict | None = None) -> list['MobileWebElement']: # type: ignore[override] - ... + def find_elements( # type: ignore[override] + self, by: str | By | RelativeBy = By.ID, value: str | dict | None = None + ) -> list['MobileWebElement']: ... def delete_extensions(self) -> None: """Delete extensions added in the class with 'setattr'""" @@ -336,7 +337,7 @@ def _update_command_executor(self, keep_alive: bool) -> None: client_config = copy.copy(self.command_executor.client_config) client_config.remote_server_addr = executor client_config.keep_alive = keep_alive - if isinstance(self.command_executor, AppiumConnection): # type: ignore + if isinstance(self.command_executor, AppiumConnection): self.command_executor = AppiumConnection(client_config=client_config) else: self.command_executor = RemoteConnection(client_config=client_config) diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 5939059c8..183dc9a1f 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -32,10 +32,9 @@ class WebElement(SeleniumWebElement): if TYPE_CHECKING: - def find_element(self, by: str = By.ID, value: str | dict | None = None) -> Self: # type: ignore[override] - ... + def find_element(self, by: str | By = By.ID, value: str | dict | None = None) -> Self: ... - def find_elements(self, by: str = By.ID, value: str | dict | None = None) -> list[Self]: # type: ignore[override] + def find_elements(self, by: str | By = By.ID, value: str | dict | None = None) -> list[Self]: # type: ignore[override] ... def get_attribute(self, name: str) -> str | dict | None: # type: ignore[override] diff --git a/test/unit/webdriver/context_test.py b/test/unit/webdriver/context_test.py index 7198984c7..77334b524 100644 --- a/test/unit/webdriver/context_test.py +++ b/test/unit/webdriver/context_test.py @@ -14,6 +14,7 @@ import httpretty +from appium.webdriver.webelement import WebElement as MobileWebElement from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body @@ -50,3 +51,17 @@ def test_switch_to_context_native_app(self): driver.switch_to.context('NATIVE_APP') assert get_httpretty_request_body(httpretty.last_request()) == {'name': 'NATIVE_APP'} + + @httpretty.activate + def test_switch_to_active_element(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/element/active'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "active-element-id"}}', + ) + + el = driver.switch_to.active_element + + assert isinstance(el, MobileWebElement) + assert el.id == 'active-element-id'