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!

1 Like

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

@René , and what about the cases where I need to check specific text under show-root?


In the screen above need to verify that text ’ You already have a company! ’ is present on the page
JS path is :
document.querySelector("#page-wrapper > div > div > app-report > div > app-web-component > div > introduction-app").shadowRoot.querySelector("section > div.info-page > div.is-size-3.has-text-weight-bold")
Is there any way to add some ‘contains’ syntax to existing JS path?
So that when running:
Page Should Contain dom: document.querySelector("#page-wrapper > div > div > app-report > div > app-web-component > div > introduction-app").shadowRoot.querySelector("section > div.info-page > div.is-size-3.has-text-weight-bold") I could receive PASS.
Thank you in advance!
`

That is an imho a real problem if using Selenium!
The Problem is, that you can not just use Xpath with selenium on an element in a shadow dom.

CSS can not select the textContent!
XPATH could but is not usable in shadowDom.

AFAIK you have 3 Options:

Option1:
Getting ALL elements that matches your selector and then loop over them to find the right one.
Be aware that you have to use querySelectorAll at the last element, so that you get all matches. In this case, all div elements

Working example:

*** Settings ***
Library    SeleniumLibrary

*** Variables ***
${ugly_selector}    dom:document.querySelector("body > ha-demo").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("app-drawer-layout > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("#view > hui-view > hui-masonry-view").shadowRoot.querySelector("#columns > div:nth-child(1) > ha-demo-card").shadowRoot.querySelectorAll("div")

*** Test Cases ***
Test
    Open Browser    https://demo.home-assistant.io/#/lovelace/0    Chrome
    ${elem}=    Get ShadowElement By Text    ${ugly_selector}    in    ARS Home
    Capture Element Screenshot     ${elem}

*** Keywords ***
Get ShadowElement By Text
    [Arguments]    ${js_selector}    ${operator}     ${search_text}
    ${elems}=    Get WebElements    ${js_selector}
    FOR    ${elem}    IN    @{elems}
        ${elem_text}=    Get Text    ${elem}
        Return From Keyword If    $search_text ${operator} $elem_text    ${elem}
    END
    FAIL    Element not found

Option2:

You do more or less the same, but directly as javaScript:
in that case you have to prefix your selector with “Array.from(” and suffix it with your finding algoritm ).find(el => el.textContent.includes("ARS Home"));
And of course you have to use querySelectorAll to get again all matching elements.

*** Variables ***
${ugly_filter_selector}=    dom:Array.from(document.querySelector("body > ha-demo").shadowRoot.querySelector("home-assistant-main").shadowRoot.querySelector("app-drawer-layout > partial-panel-resolver > ha-panel-lovelace").shadowRoot.querySelector("hui-root").shadowRoot.querySelector("#view > hui-view > hui-masonry-view").shadowRoot.querySelector("#columns > div:nth-child(1) > ha-demo-card").shadowRoot.querySelectorAll("div")).find(el => el.textContent.includes("ARS Home"));

*** Test Cases *** 
Test2
    Open Browser    https://demo.home-assistant.io/#/lovelace/0    Chrome
    Capture Element Screenshot    
    Close All Browsers

Option 3:
Say good bye to Selenium and switch to Browser Library

Browser Library is soo much much better when you have to work with shadowDom!!! :heart_eyes:
These two characters >> are to combine selectors.

It does automatically pierce shadowRoots and you can just use xpath and css and textselector as you like:

*** Settings ***
Library    Browser


*** Test Cases ***
Test3
    New Browser    headless=False
    New Page    https://demo.home-assistant.io/#/lovelace/0
    Take Screenshot    selector=ha-demo-card >> text=/ARS.*/  #textselector with regex

https://robotframework-browser.org

IMHO shadow dom is the killer for Selenium(Library) and a real usecase of Browser library

1 Like