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
16 changes: 13 additions & 3 deletions py/selenium/webdriver/remote/shadowroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
from hashlib import md5 as md5_hash
from typing import TYPE_CHECKING

from selenium.common.exceptions import InvalidSelectorException
from selenium.common.exceptions import InvalidSelectorException, NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.command import Command
from selenium.webdriver.support.relative_locator import RelativeBy

if TYPE_CHECKING:
# we only import these when the module is analyzed for type annotations
Expand Down Expand Up @@ -52,7 +53,7 @@ def __repr__(self) -> str:
def id(self) -> str:
return self._id

def find_element(self, by: str | By = By.ID, value: str | None = None) -> WebElement:
def find_element(self, by: str | By | RelativeBy = By.ID, value: str | None = None) -> WebElement:
"""Find an element inside a shadow root given a By strategy and locator.

Args:
Expand All @@ -73,6 +74,12 @@ def find_element(self, by: str | By = By.ID, value: str | None = None) -> WebEle
Example:
>>> element = driver.find_element(By.ID, "foo")
"""
if isinstance(by, RelativeBy):
elements = self.find_elements(by)
if not elements:
raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
return elements[0]

if by == By.ID:
by = By.CSS_SELECTOR
value = f'[id="{value}"]'
Expand All @@ -87,7 +94,7 @@ def find_element(self, by: str | By = By.ID, value: str | None = None) -> WebEle

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

def find_elements(self, by: str | By = By.ID, value: str | None = None) -> list[WebElement]:
def find_elements(self, by: str | By | RelativeBy = By.ID, value: str | None = None) -> list[WebElement]:
"""Find elements inside a shadow root given a By strategy and locator.

Args:
Expand All @@ -108,6 +115,9 @@ def find_elements(self, by: str | By = By.ID, value: str | None = None) -> list[
Example:
>>> element = driver.find_elements(By.ID, "foo")
"""
if isinstance(by, RelativeBy):
return self.session._find_elements_relative(by, root=self)

if by == By.ID:
by = By.CSS_SELECTOR
value = f'[id="{value}"]'
Expand Down
20 changes: 13 additions & 7 deletions py/selenium/webdriver/remote/webdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,18 +992,24 @@ def find_elements(self, by: str | By | RelativeBy = By.ID, value: str | None = N
by, value = self.locator_converter.convert(by, value)

if isinstance(by, RelativeBy):
_pkg = ".".join(__name__.split(".")[:-1])
raw_data = pkgutil.get_data(_pkg, "findElements.js")
if raw_data is None:
raise FileNotFoundError(f"Could not find findElements.js in package {_pkg}")
raw_function = raw_data.decode("utf8")
find_element_js = f"/* findElements */return ({raw_function}).apply(null, arguments);"
return self.execute_script(find_element_js, by.to_dict())
return self._find_elements_relative(by)

# Return empty list if driver returns null
# See https://github.com/SeleniumHQ/selenium/issues/4555
return self.execute(Command.FIND_ELEMENTS, {"using": by, "value": value})["value"] or []

def _find_elements_relative(self, by: RelativeBy, root: WebElement | ShadowRoot | None = None) -> list[WebElement]:
"""Find elements with a relative locator, searching from root, or the document when root is None."""
_pkg = ".".join(__name__.split(".")[:-1])
raw_data = pkgutil.get_data(_pkg, "findElements.js")
if raw_data is None:
raise FileNotFoundError(f"Could not find findElements.js in package {_pkg}")
raw_function = raw_data.decode("utf8")
find_element_js = f"/* findElements */return ({raw_function}).apply(null, arguments);"
if root is None:
return self.execute_script(find_element_js, by.to_dict())
return self.execute_script(find_element_js, by.to_dict(), root)

@property
def capabilities(self) -> dict:
"""Returns the drivers current capabilities being used."""
Expand Down
18 changes: 15 additions & 3 deletions py/selenium/webdriver/remote/webelement.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
from hashlib import md5 as md5_hash
from io import BytesIO

from selenium.common.exceptions import JavascriptException, WebDriverException
from selenium.common.exceptions import JavascriptException, NoSuchElementException, WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.common.utils import keys_to_typing
from selenium.webdriver.remote.command import Command
from selenium.webdriver.remote.shadowroot import ShadowRoot
from selenium.webdriver.support.relative_locator import RelativeBy

# TODO: Use built in importlib_resources.files.
getAttribute_js = None
Expand Down Expand Up @@ -503,7 +504,7 @@ def _execute(self, command, params=None):
params["id"] = self._id
return self._parent.execute(command, params)

def find_element(self, by: str | By = By.ID, value: str | None = None) -> WebElement:
def find_element(self, by: str | By | RelativeBy = By.ID, value: str | None = None) -> WebElement:
"""Find an element given a By strategy and locator.

Args:
Expand All @@ -525,9 +526,16 @@ def find_element(self, by: str | By = By.ID, value: str | None = None) -> WebEle
element = driver.find_element(By.ID, "foo")
"""
by, value = self._parent.locator_converter.convert(by, value)

if isinstance(by, RelativeBy):
elements = self.find_elements(by=by, value=value)
if not elements:
raise NoSuchElementException(f"Cannot locate relative element with: {by.root}")
return elements[0]

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

def find_elements(self, by: str | By = By.ID, value: str | None = None) -> list[WebElement]:
def find_elements(self, by: str | By | RelativeBy = By.ID, value: str | None = None) -> list[WebElement]:
"""Find elements given a By strategy and locator.

Args:
Expand All @@ -549,6 +557,10 @@ def find_elements(self, by: str | By = By.ID, value: str | None = None) -> list[
element = driver.find_elements(By.ID, "foo")
"""
by, value = self._parent.locator_converter.convert(by, value)

if isinstance(by, RelativeBy):
return self._parent._find_elements_relative(by, root=self)

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

def __hash__(self) -> int:
Expand Down
66 changes: 35 additions & 31 deletions py/selenium/webdriver/support/relative_locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

import warnings
from typing import NoReturn, overload
from typing import TYPE_CHECKING, NoReturn, overload

from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By, ByType
from selenium.webdriver.remote.webelement import WebElement

if TYPE_CHECKING:
from selenium.webdriver.remote.webelement import WebElement


def with_tag_name(tag_name: str) -> "RelativeBy":
def with_tag_name(tag_name: str) -> RelativeBy:
"""Start searching for relative objects using a tag name.

Args:
Expand All @@ -44,7 +48,7 @@ def with_tag_name(tag_name: str) -> "RelativeBy":
return RelativeBy({By.CSS_SELECTOR: tag_name})


def locate_with(by: ByType, using: str) -> "RelativeBy":
def locate_with(by: ByType, using: str) -> RelativeBy:
"""Start searching for relative objects your search criteria with By.

Args:
Expand Down Expand Up @@ -91,12 +95,12 @@ def __init__(self, root: dict[ByType, str] | None = None, filters: list | None =
self.filters = filters or []

@overload
def above(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def above(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def above(self, element_or_locator: None = None) -> "NoReturn": ...
def above(self, element_or_locator: None = None) -> NoReturn: ...

def above(self, element_or_locator: WebElement | LocatorType | None = None) -> "RelativeBy":
def above(self, element_or_locator: WebElement | LocatorType | None = None) -> RelativeBy:
"""Add a filter to look for elements above.

Args:
Expand All @@ -120,12 +124,12 @@ def above(self, element_or_locator: WebElement | LocatorType | None = None) -> "
return self

@overload
def below(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def below(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def below(self, element_or_locator: None = None) -> "NoReturn": ...
def below(self, element_or_locator: None = None) -> NoReturn: ...

def below(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def below(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements below.

Args:
Expand All @@ -148,12 +152,12 @@ def below(self, element_or_locator: WebElement | dict | None = None) -> "Relativ
return self

@overload
def to_left_of(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def to_left_of(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def to_left_of(self, element_or_locator: None = None) -> "NoReturn": ...
def to_left_of(self, element_or_locator: None = None) -> NoReturn: ...

def to_left_of(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def to_left_of(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements to the left of.

Args:
Expand All @@ -176,12 +180,12 @@ def to_left_of(self, element_or_locator: WebElement | dict | None = None) -> "Re
return self

@overload
def to_right_of(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def to_right_of(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def to_right_of(self, element_or_locator: None = None) -> "NoReturn": ...
def to_right_of(self, element_or_locator: None = None) -> NoReturn: ...

def to_right_of(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def to_right_of(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements right of.

Args:
Expand All @@ -204,12 +208,12 @@ def to_right_of(self, element_or_locator: WebElement | dict | None = None) -> "R
return self

@overload
def straight_above(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def straight_above(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def straight_above(self, element_or_locator: None = None) -> "NoReturn": ...
def straight_above(self, element_or_locator: None = None) -> NoReturn: ...

def straight_above(self, element_or_locator: WebElement | LocatorType | None = None) -> "RelativeBy":
def straight_above(self, element_or_locator: WebElement | LocatorType | None = None) -> RelativeBy:
"""Add a filter to look for elements above.

Args:
Expand All @@ -222,12 +226,12 @@ def straight_above(self, element_or_locator: WebElement | LocatorType | None = N
return self

@overload
def straight_below(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def straight_below(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def straight_below(self, element_or_locator: None = None) -> "NoReturn": ...
def straight_below(self, element_or_locator: None = None) -> NoReturn: ...

def straight_below(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def straight_below(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements below.

Args:
Expand All @@ -240,12 +244,12 @@ def straight_below(self, element_or_locator: WebElement | dict | None = None) ->
return self

@overload
def straight_left_of(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def straight_left_of(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def straight_left_of(self, element_or_locator: None = None) -> "NoReturn": ...
def straight_left_of(self, element_or_locator: None = None) -> NoReturn: ...

def straight_left_of(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def straight_left_of(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements to the left of.

Args:
Expand All @@ -258,12 +262,12 @@ def straight_left_of(self, element_or_locator: WebElement | dict | None = None)
return self

@overload
def straight_right_of(self, element_or_locator: WebElement | LocatorType) -> "RelativeBy": ...
def straight_right_of(self, element_or_locator: WebElement | LocatorType) -> RelativeBy: ...

@overload
def straight_right_of(self, element_or_locator: None = None) -> "NoReturn": ...
def straight_right_of(self, element_or_locator: None = None) -> NoReturn: ...

def straight_right_of(self, element_or_locator: WebElement | dict | None = None) -> "RelativeBy":
def straight_right_of(self, element_or_locator: WebElement | dict | None = None) -> RelativeBy:
"""Add a filter to look for elements right of.

Args:
Expand All @@ -276,12 +280,12 @@ def straight_right_of(self, element_or_locator: WebElement | dict | None = None)
return self

@overload
def near(self, element_or_locator: WebElement | LocatorType, distance: int = 50) -> "RelativeBy": ...
def near(self, element_or_locator: WebElement | LocatorType, distance: int = 50) -> RelativeBy: ...

@overload
def near(self, element_or_locator: None = None, distance: int = 50) -> "NoReturn": ...
def near(self, element_or_locator: None = None, distance: int = 50) -> NoReturn: ...

def near(self, element_or_locator: WebElement | LocatorType | None = None, distance: int = 50) -> "RelativeBy":
def near(self, element_or_locator: WebElement | LocatorType | None = None, distance: int = 50) -> RelativeBy:
"""Add a filter to look for elements near.

Args:
Expand Down
65 changes: 65 additions & 0 deletions py/test/selenium/webdriver/support/relative_by_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,68 @@ def test_should_find_elements_straight_right_of_another(driver, pages):
assert len(ids) == 2
assert "right" in ids
assert "center" in ids


def test_should_only_search_within_the_context_element(driver):
driver.get(
"data:text/html,"
"<div id='scope'><a id='anchor'>anchor</a><p id='inside'>inside</p></div>"
"<p id='outside'>outside</p>"
)
scope = driver.find_element(By.ID, "scope")
anchor = driver.find_element(By.ID, "anchor")

from_scope = scope.find_elements(with_tag_name("p").below(anchor))
from_driver = driver.find_elements(with_tag_name("p").below(anchor))

assert [el.get_attribute("id") for el in from_scope] == ["inside"]
assert sorted(el.get_attribute("id") for el in from_driver) == ["inside", "outside"]


def test_should_find_first_relative_element_within_the_context_element(driver):
driver.get(
"data:text/html,"
"<div id='scope'><a id='anchor'>anchor</a><p id='inside'>inside</p></div>"
"<p id='outside'>outside</p>"
)
scope = driver.find_element(By.ID, "scope")

el = scope.find_element(with_tag_name("p").below({By.ID: "anchor"}))

assert el.get_attribute("id") == "inside"
with pytest.raises(NoSuchElementException):
scope.find_element(with_tag_name("p").above({By.ID: "anchor"}))


def _load_page_with_shadow_root(driver):
driver.get(
"data:text/html,"
"<div id='host'></div>"
"<p id='outside' style='position:absolute; top:80px'>outside</p>"
"<script>"
"const root = document.getElementById('host').attachShadow({mode: 'open'});"
"root.innerHTML = \"<div id='shadow-anchor'>anchor</div><p id='inside'>inside</p>\";"
"</script>"
)
return driver.find_element(By.ID, "host").shadow_root


def test_should_only_search_within_the_shadow_root(driver):
shadow_root = _load_page_with_shadow_root(driver)
anchor = shadow_root.find_element(By.CSS_SELECTOR, "#shadow-anchor")

from_driver = driver.find_elements(with_tag_name("p").below(anchor))
from_shadow_root = shadow_root.find_elements(with_tag_name("p").below(anchor))

assert [el.get_attribute("id") for el in from_driver] == ["outside"]
assert [el.get_attribute("id") for el in from_shadow_root] == ["inside"]


def test_should_find_relative_element_in_shadow_root_by_anchor_locator(driver):
shadow_root = _load_page_with_shadow_root(driver)

el = shadow_root.find_element(with_tag_name("p").below({By.ID: "shadow-anchor"}))

assert el.get_attribute("id") == "inside"
with pytest.raises(NoSuchElementException):
shadow_root.find_element(with_tag_name("p").above({By.ID: "shadow-anchor"}))
Loading