How to add new locator strategies in SeleniumLibrary Plugin for ShadowDom

Hey Tatu (@aaltat)

Could you give me a hint how to add new locator strategies in a Selenium Plugin?
This code works but the driver instance that is handed over is a bit weird.

This ShadowDomFinder inherits the original one.
i could also just add the new finder method to the self._strategies but i was not sure.

could you maybe give a minimum example?
this _find_by_dim is just a dummy. :wink:

from SeleniumLibrary.base import LibraryComponent
from SeleniumLibrary.locators import ElementFinder


class ShadowDomFinder(ElementFinder):

    def __init__(self, ctx):
        ElementFinder.__init__(self, ctx)
        self.register('dim',self._find_by_dim, True)

    def _find_by_dim(self, driver, criteria, tag, constraints, parent=None):
        self._disallow_webelement_parent(parent)
        result = self.driver.execute_script("return %s;" % criteria)
        if result is None:
            return []
        if not isinstance(result, list):
            result = [result]
        return self._filter_elements(result, tag, constraints)


class ShadowDom(LibraryComponent):

    def __init__(self, ctx):
        LibraryComponent.__init__(self, ctx)
        self.element_finder = ShadowDomFinder(ctx)

Robot Code:

*** Settings ***
Library    SeleniumLibrary    plugins=ShadowDom

*** Test Cases ***
test
    Open Browser    browser=chrome
    Go To    chrome://settings/
    ${text}=    Get Text     dim:document.querySelector("body > settings-ui").shadowRoot.querySelector("#main").shadowRoot.querySelector("settings-basic-page").shadowRoot.querySelector("#basicPage > settings-section:nth-child(3) > settings-people-page").shadowRoot.querySelector("#edit-profile > div")
    [Teardown]    Close All Browsers

This would it with the method added to self._strategies

from SeleniumLibrary.base import LibraryComponent
from SeleniumLibrary.locators import ElementFinder


class ShadowDomFinder(ElementFinder):

    def __init__(self, ctx):
        ElementFinder.__init__(self, ctx)
        self._strategies['dim'] = self._find_by_dim

    def _find_by_dim(self, criteria, tag, constraints, parent):
        self._disallow_webelement_parent(parent)
        result = self.driver.execute_script("return %s;" % criteria)
        if result is None:
            return []
        if not isinstance(result, list):
            result = [result]
        return self._filter_elements(result, tag, constraints)


class ShadowDom(LibraryComponent):

    def __init__(self, ctx):
        LibraryComponent.__init__(self, ctx)
        self.element_finder = ShadowDomFinder(ctx)

I think the last one more close what would work. The first example has limitations on using the add_location_strategy, mainly that it has scope and those are automatically purged.

Is there something that does not work or something else?

Thanks,

The scope shall be global.
At the moment the customer is working with Add Location Strategy keyword and this adds a lot of technical keywords.

There is a cascade of locators. For each ShadowDom there is a new CSS Locator.
Sometime it looks like that a part of this cascade works, but a branch is not yet loaded.
And i want to implement a locator strategy that fetches each part of this cascade after the previous.
So that we can also deliver correct error messages, which part did not work and create a screenshot of the “last fount element” in the cascade.

Example:

document.querySelector("body > settings-ui").shadowRoot.querySelector("#main").shadowRoot.querySelector("settings-basic-page").shadowRoot.querySelector("#basicPage > settings-section:nth-child(3) > settings-people-page").shadowRoot.querySelector("#edit-profile > div")
1.    document.querySelector("body > settings-ui")
2. .shadowRoot.querySelector("#main")
3. .shadowRoot.querySelector("settings-basic-page")
4. .shadowRoot.querySelector("#basicPage > settings-section:nth-child(3) > settings-people-page")
5. .shadowRoot.querySelector("#edit-profile > div")

For example, if 4 is found, but 5 does not appear, i do want to report this.
And attach a Screenshot of 4… and the innerHTML of it.

This shadow root keeps coming up from multiple sources and SeleniumLibrary does not have proper support it. I wonder what would be good solution for it. Is the root of the shadow element always the same? If it’s we could implement some sort of root locator. Or would support for multiple locators (as a list) be idea?

I don’t have a good idea how to capture screen shot from the element, with that kind of locators.

Actually SeleniumLibrary supports ShadowDom!
The Locator Strategy dom works.

This is what is copied.

document.querySelector("body > settings-ui").shadowRoot.querySelector("#main").shadowRoot.querySelector("settings-basic-page").shadowRoot.querySelector("#basicPage > settings-section:nth-child(3) > settings-people-page").shadowRoot.querySelector("#pages > div > settings-sync-account-control").shadowRoot.querySelector("#avatar-container > img")

Use it like this

Page Should Contain Image    dom:document.querySelector("body > settings-ui").shadowRoot.querySelector("#main").shadowRoot.querySelector("settings-basic-page").shadowRoot.querySelector("#basicPage > settings-section:nth-child(3) > settings-people-page").shadowRoot.querySelector("#pages > div > settings-sync-account-control").shadowRoot.querySelector("#avatar-container > img")

This is this cascaded css selectors i was talking about.
This works if the stuff is just there.
Also Get WebElement does work.

But it would be useful to have the parent parameter available.

The capture of the Screenshot is also no problem.

But the dom locator strategy is searching this cascade at one time. I would want to have it pice by pice.

But now i know everything i need!

2 Likes

With dom selector, made easy to identify the shadow root elements… Thanks for the info.

1 Like