Created
October 11, 2025 20:27
-
-
Save emanlove/dd72f6604880f17bb0514023b1237326 to your computer and use it in GitHub Desktop.
ShadowDOM piercing locator via a plugin for SeleniumLibrary + Robot Framework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| *** Settings *** | |
| Library SeleniumLibrary plugins=${CURDIR}/ShadowDOMPlugin.py | |
| *** Test Cases *** | |
| Nested Shadow DOM | |
| Open Browser http://localhost:7272/shadow_dom.html chrome | |
| Page Should Contain Element id:shadow_host >> id:nested_shadow_host >> id:nested_div |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Shadow DOM</title> | |
| </head> | |
| <body> | |
| <div id="on_page"> | |
| <span>This is on the page DOM</span> | |
| </div></br> | |
| <div id="shadow_host"></div></br> | |
| <div id="delayed_attached"></div> | |
| <a href="javascript:return false;" onclick="attachAfterDelay(); return false;">Attach with delay</a><br/> | |
| <div id="closed_host">This is a closed host</div></br> | |
| <div id="delayed_open"></div></br> | |
| <a href="javascript:return false;" onclick="openAfterDelay(); return false;">Open with delay</a><br/> | |
| <script> | |
| let shadowRoot = document.getElementById('shadow_host').attachShadow({mode: 'open'}); | |
| shadowRoot.innerHTML = ` | |
| <span>This is within the shadow dom</span> | |
| <div id="nested_shadow_host"></div> | |
| <input type="edit" /> | |
| `; | |
| let nestedShadowRoot = shadowRoot.getElementById('nested_shadow_host').attachShadow({mode: 'open'}); | |
| nestedShadowRoot.innerHTML = ` | |
| <div id="nested_div"><div>This is within a nested shadowDOM</div></div> | |
| `; | |
| </script> | |
| <script> | |
| function attachAfterDelay() { | |
| setTimeout('attachShadowDom()', 750) | |
| }; | |
| function attachShadowDom() { | |
| let delayedAttachedShadowRoot = document.getElementById('delayed_attached').attachShadow({mode: 'open'}); | |
| delayedAttachedShadowRoot.innerHTML = ` | |
| <div id="delayed_attached_content">delayed attached</div> | |
| `; | |
| }; | |
| let delayedOpenShadowRoot = document.getElementById('delayed_open').attachShadow({mode: 'closed'}); | |
| delayedOpenShadowRoot.innerHTML = ` | |
| <div id="delayed_opened_content"> | |
| <div> | |
| This is within once/currently closed shadowDOM</br> | |
| and will be/has been opened after a delay. | |
| </div> | |
| </div> | |
| `; | |
| function openAfterDelay() { | |
| setTimeout('openShadowDom()', 750) | |
| }; | |
| function openShadowDom() { | |
| // delayedOpenShadowRoot.attachShadow({mode: 'open'}); | |
| document.getElementById('delayed_open').attachShadow({mode: 'open'}); | |
| }; | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from robot.api import logger | |
| from selenium.common.exceptions import NoSuchShadowRootException | |
| from SeleniumLibrary.errors import ElementNotFound | |
| from SeleniumLibrary.base import LibraryComponent, keyword | |
| from SeleniumLibrary.locators import ElementFinder | |
| class ShadowDOMFinder(ElementFinder): | |
| def _find(self, locator, tag=None, first_only=True, required=True, parent=None): | |
| logger.info('Using ShadowDOM enabled finder') | |
| parent_is_shadowdom_host = False | |
| element_type = "Element" if not tag else tag.capitalize() | |
| if parent and not self._is_webelement(parent): | |
| raise ValueError( | |
| f"Parent must be Selenium WebElement but it was {type(parent)}." | |
| ) | |
| if self._is_webelement(locator): | |
| return locator | |
| try: | |
| if parent: | |
| parent_is_shadowdom_host = parent.shadow_root | |
| except NoSuchShadowRootException: | |
| pass | |
| if parent_is_shadowdom_host: | |
| parent = parent_is_shadowdom_host | |
| prefix, criteria = self._parse_locator(locator) | |
| strategy = self._strategies[prefix] | |
| tag, constraints = self._get_tag_and_constraints(tag) | |
| elements = strategy(criteria, tag, constraints, parent=parent or self.driver) | |
| if required and not elements: | |
| raise ElementNotFound(f"{element_type} with locator '{locator}' not found.") | |
| if first_only: | |
| if not elements: | |
| return None | |
| return elements[0] | |
| return elements | |
| class ShadowDOMPlugin(LibraryComponent): | |
| def __init__(self, ctx): | |
| LibraryComponent.__init__(self, ctx) | |
| ctx._original_element_finder = ElementFinder(ctx) | |
| self.element_finder = ShadowDOMFinder(ctx) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment