Skip to content

[py] Support relative locators from WebElement and ShadowRoot - #18241

Open
gjreddy wants to merge 2 commits into
SeleniumHQ:trunkfrom
gjreddy:feat-13634-relative-locators-shadow-root
Open

gjreddy wants to merge 2 commits into
SeleniumHQ:trunkfrom
gjreddy:feat-13634-relative-locators-shadow-root

Conversation

@gjreddy

@gjreddy gjreddy commented Oct 25, 2026

Copy link
Copy Markdown

🔗 Related Issues

Fixes #13634

Python follow-up to #17907, which made the findElements atom accept a ShadowRoot as its root and added this for Java.

💥 What does this PR do?

Relative locators now work from a WebElement and from a ShadowRoot in the Python binding, and the search stays inside that element or shadow root:

host = driver.find_element(By.ID, "host")
shadow_root = host.shadow_root
anchor = shadow_root.find_element(By.CSS_SELECTOR, "#anchor")

shadow_root.find_elements(locate_with(By.TAG_NAME, "p").below(anchor))
element.find_element(locate_with(By.TAG_NAME, "input").to_right_of({By.ID: "label"}))

Before, both calls passed the RelativeBy object to the find-child commands and failed with TypeError: Object of type RelativeBy is not JSON serializable. The only workaround was a document-wide search from the driver, which cannot see inside a shadow root.

🔧 Implementation Notes

  • The existing driver-level code that runs the findElements atom moved into a private WebDriver._find_elements_relative(by, root=None). The driver passes no root, so its behaviour does not change.
  • WebElement and ShadowRoot call the helper with themselves as the root. The atom receives the element or shadow root reference as its second argument, the same way the Java binding does it.
  • find_element with a relative locator returns the first match, or raises NoSuchElementException, the same as WebDriver.find_element.
  • RelativeBy is imported inside the methods: relative_locator imports webelement, so a module-level import would be circular.

🧪 Tests

New tests in relative_by_tests.py, mirroring the Java tests from #17907:

  • test_should_only_search_within_the_context_element
  • test_should_find_first_relative_element_within_the_context_element
  • test_should_only_search_within_the_shadow_root
  • test_should_find_relative_element_in_shadow_root_by_anchor_locator

Results in Chrome (headless):

  • Without the change, the 4 new tests fail with Object of type RelativeBy is not JSON serializable.
  • With the change, all 31 tests in the file pass.

I ran them locally with pytest, against the 4.51.0 package with this patch applied, rather than with Bazel: building the generated BiDi modules was not possible in my environment. ruff format and ruff check pass on the changed files.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: the code changes and the tests
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

Other bindings: Java has this since #17907. I have not checked Ruby, .NET or JavaScript.

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

find_element/find_elements on a WebElement or ShadowRoot sent the
RelativeBy object straight to the find-child commands, which failed with
"Object of type RelativeBy is not JSON serializable". Run the
findElements atom with the element or shadow root as its root instead,
matching the Java binding.

Fixes SeleniumHQ#13634
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@selenium-ci selenium-ci added C-py Python Bindings B-support Issue or PR related to support classes labels Oct 25, 2026
@selenium-ci

Copy link
Copy Markdown
Member

Thank you, @gjreddy for this code suggestion.

The support packages contain example code that many users find helpful, but they do not necessarily represent
the best practices for using Selenium, and the Selenium team is not currently merging changes to them.

After reviewing the change, unless it is a critical fix or a feature that is needed for Selenium
to work, we will likely close the PR.

We actively encourage people to add the wrapper and helper code that makes sense for them to their own frameworks.
If you have any questions, please contact us

@cgoldberg cgoldberg left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of introducing a function-local import, I think you can adjust existing imports to prevent the circular import:

  • py/selenium/webdriver/support/relative_locator.py

    • Add from __future__ import annotations.
    • Move the WebElement import under if TYPE_CHECKING: to avoid the runtime dependency.
  • py/selenium/webdriver/remote/webelement.py

    • Move the RelativeBy import from TYPE_CHECKING to module level.
    • Remove the function-local import from find_element().

This eliminates the circular import while preserving the runtime isinstance() check.

relative_locator only needs WebElement for annotations, so import it under
TYPE_CHECKING. That removes the circular import, and webelement and
shadowroot can import RelativeBy at module level.
@gjreddy

gjreddy commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks @cgoldberg, that's cleaner. Done in 3ea7027:

  • relative_locator.py: added from __future__ import annotations and moved the WebElement import under TYPE_CHECKING. Ruff (UP037) then removed the quotes around the "RelativeBy"/"NoReturn" annotations, so the diff for that file is larger than the import change itself.
  • webelement.py: RelativeBy is now imported at module level, and both function-local imports are gone.
  • shadowroot.py: had the same function-local import, so I made the same change there.

Each of relative_locator, webelement, shadowroot and selenium.webdriver imports cleanly when loaded first. The relative locator tests still pass in Chrome, and ruff and mypy pass on the changed files.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-support Issue or PR related to support classes C-py Python Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🚀 Feature]: Relative Locators support for ShadowRoot SearchContext

3 participants