Asked  6 Months ago    Answers:  5   Viewed   41 times

I had been following the discussion How to automate shadow DOM elements using selenium? to work with #shadow-root (open) elements.

While in the process of locating the Clear data button within the Clear browsing data popup, which appears while accessing the url chrome://settings/clearBrowserData through Selenium I am unable to locate the following element:

#shadow-root (open)
<settings-privacy-page>

Snapshot:

settings-privacy-page

Using Selenium following are my code trials and the associated errors encountered:

  • Attempt 1:

    WebElement root5 = shadow_root4.findElement(By.tagName("settings-privacy-page"));
    
    • Error:

      Exception in thread "main" org.openqa.selenium.JavascriptException: javascript error: b.getElementsByTagName is not a function
      
  • Attempt 2:

    WebElement root5 = shadow_root4.findElement(By.cssSelector("settings-privacy-page"));
    
    • Error:

      Exception in thread "main" org.openqa.selenium.NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"settings-privacy-page"}
      
  • Attempt 3:

    WebElement root5 = (WebElement)((JavascriptExecutor)shadow_root4).executeScript("return document.getElementsByTagName('settings-privacy-page')[0]");
    
    • Error:

      Exception in thread "main" java.lang.ClassCastException: org.openqa.selenium.remote.RemoteWebElement cannot be cast to org.openqa.selenium.JavascriptExecutor
      

Incase if it is helpful the initial code block (till the above line) works perfect:

driver.get("chrome://settings/clearBrowserData");
WebElement root1 = driver.findElement(By.tagName("settings-ui"));
WebElement shadow_root1 = expand_shadow_element(root1);

WebElement root2 = shadow_root1.findElement(By.cssSelector("settings-main#main"));
WebElement shadow_root2 = expand_shadow_element(root2);

WebElement root3 = shadow_root2.findElement(By.cssSelector("settings-basic-page[role='main']"));
WebElement shadow_root3 = expand_shadow_element(root3);

WebElement root4 = shadow_root3.findElement(By.cssSelector("settings-section[page-title='Privacy and security']"));
WebElement shadow_root4 = expand_shadow_element(root4);

PS: expand_shadow_element() works flawless.

 Answers

87

If you are trying to get 'Clear Data' element then you can use the below js to get the element and then perform.

return document.querySelector('settings-ui').shadowRoot.querySelector('settings-main').shadowRoot.querySelector('settings-basic-page').shadowRoot.querySelector('settings-section > settings-privacy-page').shadowRoot.querySelector('settings-clear-browsing-data-dialog').shadowRoot.querySelector('#clearBrowsingDataDialog').querySelector('#clearBrowsingDataConfirm')

Here is the sample script.

driver.get("chrome://settings/clearBrowserData");
driver.manage().window().maximize();
JavascriptExecutor js = (JavascriptExecutor) driver; 
WebElement clearData = (WebElement) js.executeScript("return document.querySelector('settings-ui').shadowRoot.querySelector('settings-main').shadowRoot.querySelector('settings-basic-page').shadowRoot.querySelector('settings-section > settings-privacy-page').shadowRoot.querySelector('settings-clear-browsing-data-dialog').shadowRoot.querySelector('#clearBrowsingDataDialog').querySelector('#clearBrowsingDataConfirm')");
// now you can click on clear data button
clearData.click();

Edit 2: Explanation

Problem: Selenium does not provide explicit support to work with Shadow DOM elements, as they are not in the current dom. That's the reason why we will get NoSuchElementException exception when try to access the elements in the shadow dom.

Shadow DOM: enter image description here

Note: We will be referring to the terms shown in the picture. So please go through the picture for better understanding.

Solution:

In order to work with shadow element first we have to find the shadow host to which the shadow dom is attached. Here is the simple method to get the shadow root based on the shadowHost.

private static WebElement getShadowRoot(WebDriver driver,WebElement shadowHost) {
    JavascriptExecutor js = (JavascriptExecutor) driver;
    return (WebElement) js.executeScript("return arguments[0].shadowRoot", shadowHost);
}

And then you can access the shadow tree element using the shadowRoot Element.

// get the shadowHost in the original dom using findElement
WebElement shadowHost = driver.findElement(By.cssSelector("shadowHost_CSS"));
// get the shadow root
WebElement shadowRoot = getShadowRoot(driver,shadowHost);
// access shadow tree element
WebElement shadowTreeElement = shadowRoot.findElement(By.cssSelector("shadow_tree_element_css"));

In order to simplify all the above steps created the below method.

public static WebElement getShadowElement(WebDriver driver,WebElement shadowHost, String cssOfShadowElement) {
    WebElement shardowRoot = getShadowRoot(driver, shadowHost);
    return shardowRoot.findElement(By.cssSelector(cssOfShadowElement));
}

Now you can get the shadowTree Element with single method call

WebElement shadowHost = driver.findElement(By.cssSelector("shadowHost_CSS_Goes_here));
WebElement shadowTreeElement = getShadowElement(driver,shadowHost,"shadow_tree_element_css");

And perform the operations as usual like .click(), .getText().

shadowTreeElement.click()

This Looks simple when you have only one level of shadow DOM. But here, in this case we have multiple levels of shadow doms. So we have to access the element by reaching each shadow host and root. enter image description here

Below is the snippet using the methods that mentioned above (getShadowElement and getShadowRoot)

// Locate shadowHost on the current dom
WebElement shadowHostL1 = driver.findElement(By.cssSelector("settings-ui"));

// now locate the shadowElement by traversing all shadow levels
WebElement shadowElementL1 = getShadowElement(driver, shadowHostL1, "settings-main");
WebElement shadowElementL2 = getShadowElement(driver, shadowElementL1,"settings-basic-page");
WebElement shadowElementL3 = getShadowElement(driver, shadowElementL2,"settings-section > settings-privacy-page");
WebElement shadowElementL4 = getShadowElement(driver, shadowElementL3,"settings-clear-browsing-data-dialog");
WebElement shadowElementL5 = getShadowElement(driver, shadowElementL4,"#clearBrowsingDataDialog");
WebElement clearData = shadowElementL5.findElement(By.cssSelector("#clearBrowsingDataConfirm"));
System.out.println(clearData.getText());
clearData.click();

You can achieve all the above steps in single js call as at mentioned at the beginning of the answer (added below just to reduce the confusion).

WebElement clearData = (WebElement) js.executeScript("return document.querySelector('settings-ui').shadowRoot.querySelector('settings-main').shadowRoot.querySelector('settings-basic-page').shadowRoot.querySelector('settings-section > settings-privacy-page').shadowRoot.querySelector('settings-clear-browsing-data-dialog').shadowRoot.querySelector('#clearBrowsingDataDialog').querySelector('#clearBrowsingDataConfirm')");

Screenshot: enter image description here

Tuesday, June 1, 2021
 
Troncoso
answered 6 Months ago
12
ul > li

only does the immediate children. So, for example, to do only the top level list elements you could use:

#parent > li

Note: this isn't supported on IE6.

The common workaround for backwards compatibility is to do something like this:

#parent li { /* style appropriately */ }
#parent li li { /* back to normal */ }

It's more tedious because you have to apply styles and then turn them off (and you may not necessarily know what the old values are) but it's the only IE6-friendly pure CSS workaround there is.

Edit: Ok you have a MooTools specific issue. getElements() returns all descendants, not just immediate children. Try using getChildren().

var drop = function(el){
    el.getParents('ul').reverse().each(function(item){
        var posCount = 1;
        item.getChildren("li").getElements("a span[class=position]").each(function(pos){
                pos.set('text', posCount);
                posCount++;
        });
    });
}

or something like that.

Monday, June 14, 2021
 
pwaring
answered 6 Months ago
88

Use this selector: :host > *

The :host selector is described in https://drafts.csswg.org/css-scoping/#host-selector

document.querySelector( 'my-element' )
  .attachShadow( { mode: 'open' } )
  .innerHTML = `
    <span>SPAN</span>
    <a>A</a>
    <p>P</p>
    <div>
      DIV
      <span>SPAN IN DIV</span>
    </div>
    <style>
      :host>*{border:1px red solid;}
    </style>`
<my-element>
</my-element>

:host may also hold a compound selector, which must be places in brackets. E.g. :host([foo=bar]) selects a host element which has attribute foo set to bar.

Thursday, August 12, 2021
 
nhunston
answered 4 Months ago
50

Different iframes have different contexts and documents: when you invoke $('#btnInside'), jQuery is executing in the host iframe and looking in that document, so it can't find it. If the nested iframe is on the same server though, you can tunnel through to its document from the host document:

$(document).ready(function() {
    $("#btnOutside").click(function () {
        $('iframe[src="other.html"]').contents().find("#btnInside").click();
    });
});
Sunday, August 15, 2021
 
AnchovyLegend
answered 4 Months ago
14

In addition to what pm_2 mentioned, you could take advantage of MVVMLight's Messenger class. The VM can send a message that is received by the View to change the background.

public class ChangeBackgroundMessage
{
    public Brush TheColor { get; set; } 
} 

And then in your VM:

Button1Command = new RelayCommand(() => ExecuteButtonCommand());

....

private void ExecuteButtonCommand()
{
    Messenger.Default.Send<ChangeBackgroundMessage>(new ChangeBackgroundMessage { TheColor = Brushes.Red } );
} 

and in your View:

public partial class MyView : UserControl
{
    public MyView()
    {
         InitializeComponent();
         Messenger.Default.Register<ChangeBackgroundMessage>(this, m => ReceiveChangeBackgroundMessage(m);
    } 

    private void ReceiveChangeBackgroundMessage(ChangeBackgroundMessage m)
    {
          // If you need to ensure this executes only on UI thread, use the
          // DispatcherHelper class

          DispatcherHelper.CheckBeginInvokeOnUI(() => button2.Background = m.TheColor);
    }

}

Yet another alternative would be to have a "view service" that the View registers with it's ViewModel. For example:

public interface IMySpecificViewService
{ 
    void ChangeButtonColor(Brush color);
} 

In VM:

public IMySpecificViewService ViewService { get; set; } 

and in View

public partial class MyView : UserControl, IMySpecificViewService
...
public MyView()
{ 
    var vm = (MyViewModel)this.DataContext;
    vm.ViewService = (IMySpecificViewService)this;
} 

public void ChangeButtonColor(Brush color)
{
    Button2.Background = color;
}  

which can be called in your VM's command handler:

private void ExecuteButtonCommand()
{
    ViewService?.ChangeButtonColor(Brushes.Red);
} 

I find I use these approaches when I can't directly bind to a property in the VM, (or I don't want to bleed any View specific stuff in the VM) and I need more fine grained control over manipulating the controls.

Wednesday, August 25, 2021
 
iceduck
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share