From 9991ebc8c59ffd60ff0332048c2fdaff711ce8a4 Mon Sep 17 00:00:00 2001 From: Dor Blayzer Date: Sun, 20 Sep 2026 13:07:00 +0200 Subject: [PATCH] fix(webelement): preserve default locator strategy in webelement type hints The type-only find_element and find_elements declarations in WebElement and CanFindElements required by, although their inherited Selenium implementations default it to By.ID. This makes static type checkers reject valid calls such as element.find_element(value='child') with a missing-argument error. Add the inherited default locator strategy (by: str = By.ID) to WebElement and CanFindElements, matching WebDriver. Add unit tests covering omitted locator strategy dispatch and protocol conformance. --- .../protocols/webdriver/can_find_elements.py | 6 ++- appium/webdriver/webelement.py | 7 ++- test/unit/webdriver/webelement_test.py | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/appium/protocols/webdriver/can_find_elements.py b/appium/protocols/webdriver/can_find_elements.py index 01cbe5b4..8ad6d5a5 100644 --- a/appium/protocols/webdriver/can_find_elements.py +++ b/appium/protocols/webdriver/can_find_elements.py @@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable +from selenium.webdriver.common.by import By + if TYPE_CHECKING: from appium.webdriver.webelement import WebElement @@ -27,6 +29,6 @@ class CanFindElements(Protocol): - find_elements(by, value): Find multiple elements """ - def find_element(self, by: str, value: str | dict | None = None) -> 'WebElement': ... + def find_element(self, by: str = By.ID, value: str | dict | None = None) -> 'WebElement': ... - def find_elements(self, by: str, value: str | dict | None = None) -> list['WebElement']: ... + def find_elements(self, by: str = By.ID, value: str | dict | None = None) -> list['WebElement']: ... diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 8494af13..5939059c 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -22,6 +22,9 @@ from .mobilecommand import MobileCommand as Command +if TYPE_CHECKING: + from selenium.webdriver.common.by import By + class WebElement(SeleniumWebElement): _execute: Callable @@ -29,10 +32,10 @@ class WebElement(SeleniumWebElement): if TYPE_CHECKING: - def find_element(self, by: str, value: str | dict | None = None) -> Self: # type: ignore[override] + def find_element(self, by: str = By.ID, value: str | dict | None = None) -> Self: # type: ignore[override] ... - def find_elements(self, by: str, value: str | dict | None = None) -> list[Self]: # type: ignore[override] + def find_elements(self, by: str = 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/webelement_test.py b/test/unit/webdriver/webelement_test.py index a37c2ec6..be9c8857 100644 --- a/test/unit/webdriver/webelement_test.py +++ b/test/unit/webdriver/webelement_test.py @@ -17,6 +17,7 @@ import httpretty +from appium.protocols.webdriver.can_find_elements import CanFindElements from appium.webdriver.webelement import WebElement as MobileWebElement from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body @@ -101,3 +102,50 @@ def test_element_location_in_view(self): httpretty.last_request() assert loc == location_in_view + + @httpretty.activate + def test_find_element_default_by(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}', + ) + + el = element.find_element(value='child_id') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'child_id' + assert isinstance(el, MobileWebElement) + assert el.id == 'child-element-id' + + @httpretty.activate + def test_find_elements_default_by(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-1"}, {"element-6066-11e4-a52e-4f735466cecf": "child-2"}]}', + ) + + els = element.find_elements(value='child_ids') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'child_ids' + assert len(els) == 2 + assert isinstance(els[0], MobileWebElement) + assert els[0].id == 'child-1' + assert isinstance(els[1], MobileWebElement) + assert els[1].id == 'child-2' + + @httpretty.activate + def test_can_find_elements_protocol_conformance(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + + assert isinstance(element, CanFindElements) + assert isinstance(driver, CanFindElements)