Skip to content

Commit 7caac87

Browse files
committed
Fix : RelativeBy Annotations
1 parent cc3ea7b commit 7caac87

File tree

6 files changed

+159
-48
lines changed

6 files changed

+159
-48
lines changed

py/selenium/webdriver/remote/shadowroot.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
# under the License.
1717

1818
from hashlib import md5 as md5_hash
19+
from typing import Union, TYPE_CHECKING
20+
if TYPE_CHECKING:
21+
from selenium.webdriver.support.relative_locator import RelativeBy
1922

20-
from ..common.by import By
23+
from ..common.by import By, ByType
2124
from .command import Command
2225

2326

@@ -43,7 +46,7 @@ def __repr__(self) -> str:
4346
def id(self) -> str:
4447
return self._id
4548

46-
def find_element(self, by: str = By.ID, value: str = None):
49+
def find_element(self, by: "Union[ByType, RelativeBy]" = By.ID, value: str = None):
4750
"""Find an element inside a shadow root given a By strategy and
4851
locator.
4952
@@ -82,7 +85,7 @@ def find_element(self, by: str = By.ID, value: str = None):
8285

8386
return self._execute(Command.FIND_ELEMENT_FROM_SHADOW_ROOT, {"using": by, "value": value})["value"]
8487

85-
def find_elements(self, by: str = By.ID, value: str = None):
88+
def find_elements(self, by: "Union[ByType, RelativeBy]" = By.ID, value: str = None):
8689
"""Find elements inside a shadow root given a By strategy and locator.
8790
8891
Parameters:

py/selenium/webdriver/remote/webdriver.py

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
from base64 import b64decode, urlsafe_b64encode
3131
from contextlib import asynccontextmanager, contextmanager
3232
from importlib import import_module
33-
from typing import Any, Optional, Union
33+
from typing import Any, Optional, Union, TYPE_CHECKING
34+
35+
if TYPE_CHECKING:
36+
from selenium.webdriver.support.relative_locator import RelativeBy
3437

3538
from selenium.common.exceptions import (
3639
InvalidArgumentException,
@@ -42,12 +45,11 @@
4245
from selenium.webdriver.common.bidi.browser import Browser
4346
from selenium.webdriver.common.bidi.browsing_context import BrowsingContext
4447
from selenium.webdriver.common.bidi.network import Network
45-
from selenium.webdriver.common.bidi.permissions import Permissions
4648
from selenium.webdriver.common.bidi.script import Script
4749
from selenium.webdriver.common.bidi.session import Session
4850
from selenium.webdriver.common.bidi.storage import Storage
4951
from selenium.webdriver.common.bidi.webextension import WebExtension
50-
from selenium.webdriver.common.by import By
52+
from selenium.webdriver.common.by import By, ByType
5153
from selenium.webdriver.common.options import ArgOptions, BaseOptions
5254
from selenium.webdriver.common.print_page_options import PrintOptions
5355
from selenium.webdriver.common.timeouts import Timeouts
@@ -56,8 +58,6 @@
5658
VirtualAuthenticatorOptions,
5759
required_virtual_authenticator,
5860
)
59-
from selenium.webdriver.support.relative_locator import RelativeBy
60-
6161
from ..common.fedcm.dialog import Dialog
6262
from .bidi_connection import BidiConnection
6363
from .client_config import ClientConfig
@@ -266,7 +266,6 @@ def __init__(
266266
self._browsing_context = None
267267
self._storage = None
268268
self._webextension = None
269-
self._permissions = None
270269

271270
def __repr__(self):
272271
return f'<{type(self).__module__}.{type(self).__name__} (session="{self.session_id}")>'
@@ -879,7 +878,7 @@ def timeouts(self, timeouts) -> None:
879878
"""
880879
_ = self.execute(Command.SET_TIMEOUTS, timeouts._to_json())["value"]
881880

882-
def find_element(self, by=By.ID, value: Optional[str] = None) -> WebElement:
881+
def find_element(self, by: "Union[ByType, RelativeBy]" = By.ID, value: Optional[str] = None) -> WebElement:
883882
"""Find an element given a By strategy and locator.
884883
885884
Parameters:
@@ -915,7 +914,7 @@ def find_element(self, by=By.ID, value: Optional[str] = None) -> WebElement:
915914

916915
return self.execute(Command.FIND_ELEMENT, {"using": by, "value": value})["value"]
917916

918-
def find_elements(self, by=By.ID, value: Optional[str] = None) -> list[WebElement]:
917+
def find_elements(self, by: "Union[ByType, RelativeBy]" = By.ID, value: Optional[str] = None) -> list[WebElement]:
919918
"""Find elements given a By strategy and locator.
920919
921920
Parameters:
@@ -1341,28 +1340,6 @@ def storage(self):
13411340

13421341
return self._storage
13431342

1344-
@property
1345-
def permissions(self):
1346-
"""Returns a permissions module object for BiDi permissions commands.
1347-
1348-
Returns:
1349-
--------
1350-
Permissions: an object containing access to BiDi permissions commands.
1351-
1352-
Examples:
1353-
---------
1354-
>>> from selenium.webdriver.common.bidi.permissions import PermissionDescriptor, PermissionState
1355-
>>> descriptor = PermissionDescriptor("geolocation")
1356-
>>> driver.permissions.set_permission(descriptor, PermissionState.GRANTED, "https://example.com")
1357-
"""
1358-
if not self._websocket_connection:
1359-
self._start_bidi()
1360-
1361-
if self._permissions is None:
1362-
self._permissions = Permissions(self._websocket_connection)
1363-
1364-
return self._permissions
1365-
13661343
@property
13671344
def webextension(self):
13681345
"""Returns a webextension module object for BiDi webextension commands.

py/selenium/webdriver/remote/webelement.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@
2424
from base64 import b64decode, encodebytes
2525
from hashlib import md5 as md5_hash
2626
from io import BytesIO
27+
from typing import Union, TYPE_CHECKING
28+
29+
if TYPE_CHECKING:
30+
from selenium.webdriver.support.relative_locator import RelativeBy
31+
2732

2833
from selenium.common.exceptions import JavascriptException, WebDriverException
29-
from selenium.webdriver.common.by import By
34+
from selenium.webdriver.common.by import By, ByType
3035
from selenium.webdriver.common.utils import keys_to_typing
3136

3237
from .command import Command
@@ -572,7 +577,7 @@ def _execute(self, command, params=None):
572577
params["id"] = self._id
573578
return self._parent.execute(command, params)
574579

575-
def find_element(self, by=By.ID, value=None) -> WebElement:
580+
def find_element(self, by: "Union[ByType, RelativeBy]" = By.ID, value=None) -> WebElement:
576581
"""Find an element given a By strategy and locator.
577582
578583
Parameters:
@@ -601,7 +606,7 @@ def find_element(self, by=By.ID, value=None) -> WebElement:
601606
by, value = self._parent.locator_converter.convert(by, value)
602607
return self._execute(Command.FIND_CHILD_ELEMENT, {"using": by, "value": value})["value"]
603608

604-
def find_elements(self, by=By.ID, value=None) -> list[WebElement]:
609+
def find_elements(self, by: "Union[ByType, RelativeBy]" = By.ID, value=None) -> list[WebElement]:
605610
"""Find elements given a By strategy and locator.
606611
607612
Parameters:

py/selenium/webdriver/support/expected_conditions.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
import re
1919
from collections.abc import Iterable
20-
from typing import Any, Callable, Literal, TypeVar, Union
20+
from typing import Any, Callable, Literal, TypeVar, Union, Tuple
21+
22+
from selenium.webdriver.common.by import ByType
23+
from selenium.webdriver.support.relative_locator import RelativeBy
2124

2225
from selenium.common.exceptions import (
2326
NoAlertPresentException,
@@ -38,6 +41,7 @@
3841
T = TypeVar("T")
3942

4043
WebDriverOrWebElement = Union[WebDriver, WebElement]
44+
LocatorType = Union[Tuple[ByType, str], Tuple[RelativeBy, None]]
4145

4246

4347
def title_is(title: str) -> Callable[[WebDriver], bool]:
@@ -79,7 +83,7 @@ def _predicate(driver: WebDriver):
7983
return _predicate
8084

8185

82-
def presence_of_element_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], WebElement]:
86+
def presence_of_element_located(locator: LocatorType) -> Callable[[WebDriverOrWebElement], WebElement]:
8387
"""An expectation for checking that an element is present on the DOM of a
8488
page. This does not necessarily mean that the element is visible.
8589
@@ -189,7 +193,7 @@ def _predicate(driver: WebDriver):
189193

190194

191195
def visibility_of_element_located(
192-
locator: tuple[str, str],
196+
locator: LocatorType,
193197
) -> Callable[[WebDriverOrWebElement], Union[Literal[False], WebElement]]:
194198
"""An expectation for checking that an element is present on the DOM of a
195199
page and visible. Visibility means that the element is not only displayed
@@ -272,7 +276,7 @@ def _element_if_visible(element: WebElement, visibility: bool = True) -> Union[L
272276
return element if element.is_displayed() == visibility else False
273277

274278

275-
def presence_of_all_elements_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
279+
def presence_of_all_elements_located(locator: LocatorType) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
276280
"""An expectation for checking that there is at least one element present
277281
on a web page.
278282
@@ -299,7 +303,7 @@ def _predicate(driver: WebDriverOrWebElement):
299303
return _predicate
300304

301305

302-
def visibility_of_any_elements_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
306+
def visibility_of_any_elements_located(locator: LocatorType) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
303307
"""An expectation for checking that there is at least one element visible
304308
on a web page.
305309
@@ -327,7 +331,7 @@ def _predicate(driver: WebDriverOrWebElement):
327331

328332

329333
def visibility_of_all_elements_located(
330-
locator: tuple[str, str],
334+
locator: LocatorType,
331335
) -> Callable[[WebDriverOrWebElement], Union[list[WebElement], Literal[False]]]:
332336
"""An expectation for checking that all elements are present on the DOM of
333337
a page and visible. Visibility means that the elements are not only
@@ -363,7 +367,7 @@ def _predicate(driver: WebDriverOrWebElement):
363367
return _predicate
364368

365369

366-
def text_to_be_present_in_element(locator: tuple[str, str], text_: str) -> Callable[[WebDriverOrWebElement], bool]:
370+
def text_to_be_present_in_element(locator: LocatorType, text_: str) -> Callable[[WebDriverOrWebElement], bool]:
367371
"""An expectation for checking if the given text is present in the
368372
specified element.
369373
@@ -399,7 +403,7 @@ def _predicate(driver: WebDriverOrWebElement):
399403

400404

401405
def text_to_be_present_in_element_value(
402-
locator: tuple[str, str], text_: str
406+
locator: LocatorType, text_: str
403407
) -> Callable[[WebDriverOrWebElement], bool]:
404408
"""An expectation for checking if the given text is present in the
405409
element's value.
@@ -436,7 +440,7 @@ def _predicate(driver: WebDriverOrWebElement):
436440

437441

438442
def text_to_be_present_in_element_attribute(
439-
locator: tuple[str, str], attribute_: str, text_: str
443+
locator: LocatorType, attribute_: str, text_: str
440444
) -> Callable[[WebDriverOrWebElement], bool]:
441445
"""An expectation for checking if the given text is present in the
442446
element's attribute.
@@ -687,7 +691,7 @@ def _predicate(_):
687691
return _predicate
688692

689693

690-
def element_located_to_be_selected(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], bool]:
694+
def element_located_to_be_selected(locator: LocatorType) -> Callable[[WebDriverOrWebElement], bool]:
691695
"""An expectation for the element to be located is selected.
692696
693697
Parameters:
@@ -743,7 +747,7 @@ def _predicate(_):
743747

744748

745749
def element_located_selection_state_to_be(
746-
locator: tuple[str, str], is_selected: bool
750+
locator: LocatorType, is_selected: bool
747751
) -> Callable[[WebDriverOrWebElement], bool]:
748752
"""An expectation to locate an element and check if the selection state
749753
specified is in that state.
@@ -858,7 +862,7 @@ def _predicate(driver: WebDriver):
858862
return _predicate
859863

860864

861-
def element_attribute_to_include(locator: tuple[str, str], attribute_: str) -> Callable[[WebDriverOrWebElement], bool]:
865+
def element_attribute_to_include(locator: LocatorType, attribute_: str) -> Callable[[WebDriverOrWebElement], bool]:
862866
"""An expectation for checking if the given attribute is included in the
863867
specified element.
864868
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
from selenium.webdriver.support import expected_conditions as EC
3+
from selenium.webdriver.support.relative_locator import locate_with
4+
from selenium.webdriver.common.by import By
5+
from unittest.mock import Mock
6+
7+
8+
class TestExpectedConditionsRelativeBy:
9+
"""Test that expected conditions accept RelativeBy in type annotations"""
10+
11+
def test_presence_of_element_located_accepts_relative_by(self):
12+
"""Test presence_of_element_located accepts RelativeBy"""
13+
relative_by = locate_with(By.TAG_NAME, "div").above({By.ID: "footer"})
14+
condition = EC.presence_of_element_located(relative_by)
15+
assert condition is not None
16+
17+
def test_visibility_of_element_located_accepts_relative_by(self):
18+
"""Test visibility_of_element_located accepts RelativeBy"""
19+
relative_by = locate_with(By.TAG_NAME, "button").near({By.CLASS_NAME: "submit"})
20+
condition = EC.visibility_of_element_located(relative_by)
21+
assert condition is not None
22+
23+
def test_presence_of_all_elements_located_accepts_relative_by(self):
24+
"""Test presence_of_all_elements_located accepts RelativeBy"""
25+
relative_by = locate_with(By.CSS_SELECTOR, ".item").below({By.ID: "header"})
26+
condition = EC.presence_of_all_elements_located(relative_by)
27+
assert condition is not None
28+
29+
def test_visibility_of_any_elements_located_accepts_relative_by(self):
30+
"""Test visibility_of_any_elements_located accepts RelativeBy"""
31+
relative_by = locate_with(By.TAG_NAME, "span").to_left_of({By.ID: "sidebar"})
32+
condition = EC.visibility_of_any_elements_located(relative_by)
33+
assert condition is not None
34+
35+
def test_text_to_be_present_in_element_accepts_relative_by(self):
36+
"""Test text_to_be_present_in_element accepts RelativeBy"""
37+
relative_by = locate_with(By.TAG_NAME, "p").above({By.CLASS_NAME: "footer"})
38+
condition = EC.text_to_be_present_in_element(relative_by, "Hello")
39+
assert condition is not None
40+
41+
def test_element_to_be_clickable_accepts_relative_by(self):
42+
"""Test element_to_be_clickable accepts RelativeBy"""
43+
relative_by = locate_with(By.TAG_NAME, "button").near({By.ID: "form"})
44+
condition = EC.element_to_be_clickable(relative_by)
45+
assert condition is not None
46+
47+
def test_invisibility_of_element_located_accepts_relative_by(self):
48+
"""Test invisibility_of_element_located accepts RelativeBy"""
49+
relative_by = locate_with(By.CSS_SELECTOR, ".loading").above({By.ID: "content"})
50+
condition = EC.invisibility_of_element_located(relative_by)
51+
assert condition is not None
52+
53+
def test_element_located_to_be_selected_accepts_relative_by(self):
54+
"""Test element_located_to_be_selected accepts RelativeBy"""
55+
relative_by = locate_with(By.TAG_NAME, "input").near({By.ID: "terms-label"})
56+
condition = EC.element_located_to_be_selected(relative_by)
57+
assert condition is not None
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pytest
2+
from selenium.webdriver.common.by import By
3+
from selenium.webdriver.support.relative_locator import RelativeBy, locate_with
4+
from selenium.webdriver.remote.webdriver import WebDriver
5+
from selenium.webdriver.remote.webelement import WebElement
6+
from selenium.webdriver.remote.shadowroot import ShadowRoot
7+
from unittest.mock import Mock, MagicMock
8+
9+
10+
class TestRelativeByAnnotations:
11+
"""Test that RelativeBy is properly accepted in type annotations"""
12+
13+
def test_webdriver_find_element_accepts_relative_by(self):
14+
"""Test WebDriver.find_element accepts RelativeBy"""
15+
driver = Mock(spec=WebDriver)
16+
relative_by = locate_with(By.TAG_NAME, "div").above({By.ID: "footer"})
17+
18+
# This should not raise type checking errors
19+
driver.find_element(by=relative_by)
20+
driver.find_element(relative_by)
21+
22+
def test_webdriver_find_elements_accepts_relative_by(self):
23+
"""Test WebDriver.find_elements accepts RelativeBy"""
24+
driver = Mock(spec=WebDriver)
25+
relative_by = locate_with(By.TAG_NAME, "div").below({By.ID: "header"})
26+
27+
# This should not raise type checking errors
28+
driver.find_elements(by=relative_by)
29+
driver.find_elements(relative_by)
30+
31+
def test_webelement_find_element_accepts_relative_by(self):
32+
"""Test WebElement.find_element accepts RelativeBy"""
33+
element = Mock(spec=WebElement)
34+
relative_by = locate_with(By.TAG_NAME, "span").near({By.CLASS_NAME: "button"})
35+
36+
# This should not raise type checking errors
37+
element.find_element(by=relative_by)
38+
element.find_element(relative_by)
39+
40+
def test_webelement_find_elements_accepts_relative_by(self):
41+
"""Test WebElement.find_elements accepts RelativeBy"""
42+
element = Mock(spec=WebElement)
43+
relative_by = locate_with(By.TAG_NAME, "input").to_left_of({By.ID: "submit"})
44+
45+
# This should not raise type checking errors
46+
element.find_elements(by=relative_by)
47+
element.find_elements(relative_by)
48+
49+
def test_shadowroot_find_element_accepts_relative_by(self):
50+
"""Test ShadowRoot.find_element accepts RelativeBy"""
51+
shadow_root = Mock(spec=ShadowRoot)
52+
relative_by = locate_with(By.TAG_NAME, "button").to_right_of({By.ID: "cancel"})
53+
54+
# This should not raise type checking errors
55+
shadow_root.find_element(by=relative_by)
56+
shadow_root.find_element(relative_by)
57+
58+
def test_shadowroot_find_elements_accepts_relative_by(self):
59+
"""Test ShadowRoot.find_elements accepts RelativeBy"""
60+
shadow_root = Mock(spec=ShadowRoot)
61+
relative_by = locate_with(By.CSS_SELECTOR, ".item").above({By.ID: "footer"})
62+
63+
# This should not raise type checking errors
64+
shadow_root.find_elements(by=relative_by)
65+
shadow_root.find_elements(relative_by)

0 commit comments

Comments
 (0)