javascript 使用 Selenium 访问 Shadow DOM 树

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23920167/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 01:48:36  来源:igfitidea点击:

Accessing Shadow DOM tree with Selenium

javascriptpythonseleniumselenium-chromedrivershadow-dom

提问by lambinator

Is it possible to access elements within a Shadow DOM using Selenium/Chrome webdriver?

是否可以使用 Selenium/Chrome webdriver 访问 Shadow DOM 中的元素?

Using the normal element search methods doesn't work, as is to be expected. I've seen references to the switchToSubTreespec on w3c, but couldn't locate any actual docs, examples, etc.

正如预期的那样,使用普通元素搜索方法不起作用。我在 w3c 上看到了对switchToSubTree规范的引用,但找不到任何实际的文档、示例等。

Anyone had success with this?

有人成功了吗?

采纳答案by Damen TheSifter

回答by Eduard Florinescu

The accepted answer is no longer valid and some of the other answers have some drawbacks or are not practical (the /deep/selector doesn't work and is deprecated, document.querySelector('').shadowRootworks only with the first shadow element when shadow elements are nested), sometimes the shadow root elements are nested and the second shadow root is not visible in document root, but is available in its parent accessed shadow root. I think is better to use the selenium selectors and inject the script just to take the shadow root:

接受的答案不再有效,其他一些答案有一些缺点或不实用(/deep/选择器不起作用并且已弃用,document.querySelector('').shadowRoot仅当阴影元素嵌套时才与第一个阴影元素一起使用),有时阴影根元素是嵌套的,第二个影子根在文档根中不可见,但在其父访问的影子根中可用。我认为最好使用 selenium 选择器并注入脚本以获取影子根:

def expand_shadow_element(element):
  shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
  return shadow_root

outer = expand_shadow_element(driver.find_element_by_css_selector("#test_button"))
inner = outer.find_element_by_id("inner_button")
inner.click()

To put this into perspective I just added a testable example with Chrome's download page, clicking the search button needs open 3 nested shadow root elements: enter image description here

为了理解这一点,我刚刚在 Chrome 的下载页面中添加了一个可测试的示例,单击搜索按钮需要打开 3 个嵌套的阴影根元素: 在此处输入图片说明

import selenium
from selenium import webdriver
driver = webdriver.Chrome()


def expand_shadow_element(element):
  shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
  return shadow_root

driver.get("chrome://downloads")
root1 = driver.find_element_by_tag_name('downloads-manager')
shadow_root1 = expand_shadow_element(root1)

root2 = shadow_root1.find_element_by_css_selector('downloads-toolbar')
shadow_root2 = expand_shadow_element(root2)

root3 = shadow_root2.find_element_by_css_selector('cr-search-field')
shadow_root3 = expand_shadow_element(root3)

search_button = shadow_root3.find_element_by_css_selector("#search-button")
search_button.click()

Doing the same approach suggested in the other answers has the drawback that it hard-codes the queries, is less readable and you cannot use the intermediary selections for other actions:

执行其他答案中建议的相同方法的缺点是它对查询进行了硬编码,可读性较差,并且您不能将中间选择用于其他操作:

search_button = driver.execute_script('return document.querySelector("downloads-manager").shadowRoot.querySelector("downloads-toolbar").shadowRoot.querySelector("cr-search-field").shadowRoot.querySelector("#search-button")')
search_button.click()

回答by djangofan

It should also be noted that the Selenium binary Chrome driver now supports Shadow DOM (since Jan 28, 2015) : http://chromedriver.storage.googleapis.com/2.14/notes.txt

还应该注意的是,Selenium 二进制 Chrome 驱动程序现在支持 Shadow DOM(自 2015 年 1 月 28 日起):http: //chromedriver.storage.googleapis.com/2.14/notes.txt

回答by Erik Selin

I am using C# and Selenium and managed to find an element inside a nestled shadow DOM using java script. This is my html tree:

我正在使用 C# 和 Selenium,并设法使用 java 脚本在嵌套的 shadow DOM 中找到一个元素。这是我的 html 树:

html tree

html 树

I want the url on the last line and to get it I first select the "downloads-manager" tag and then the first shadow root right below it. Once inside the shadow root I want to find the element closest to the next shadow root. That element is "downloads-item". With that selected I can enter the second shadow root. From there I select the img item containing the url by id = "file-icon". At last I can get the attribute "src" which contains the url I am seeking.

我想要最后一行的 url 并获得它,我首先选择“downloads-manager”标签,然后选择它下方的第一个影子根。一旦进入阴影根,我想找到最接近下一个阴影根的元素。该元素是“下载项”。选择后,我可以进入第二个影子根。从那里我通过 id = "file-icon" 选择包含 url 的 img 项目。最后,我可以获得包含我正在寻找的 url 的属性“src”。

The two lines of C# code that does the trick:

两行 C# 代码可以解决这个问题:

IJavaScriptExecutor jse2 = (IJavaScriptExecutor)_driver;
var pdfUrl = jse2.ExecuteScript("return document.querySelector('downloads-manager').shadowRoot.querySelector('downloads-item').shadowRoot.getElementById('file-icon').getAttribute('src')");

回答by Akash Jha

I found a much easier way to get the elements from Shadow Dom. I am taking the same example given above, for search iconof Chrome Download Page.

我找到了一种更简单的方法来从 Shadow Dom 中获取元素。我正在上面给出,对于同样的例子搜索图标Chrome的下载页面

IWebDriver driver;

public IWebElement getUIObject(params By[] shadowRoots)
        {
            IWebElement currentElement = null;
            IWebElement parentElement = null;
            int i = 0;
            foreach (var item in shadowRoots)
            {
                if (parentElement == null)
                {
                    currentElement = driver.FindElement(item);
                }
                else
                {
                    currentElement = parentElement.FindElement(item);
                }
                if(i !=(shadowRoots.Length-1))
                {
                    parentElement = expandRootElement(currentElement);
                }
                i++;
            }
            return currentElement;
        }

 public IWebElement expandRootElement(IWebElement element)
        {
            IWebElement rootElement = (IWebElement)((IJavaScriptExecutor)driver)
        .ExecuteScript("return arguments[0].shadowRoot", element);
            return rootElement;
        }

Google Chrome Download Page

谷歌浏览器下载页面

Now as shown in image we have to expand three shadow root elements in order to get our search icon. To to click on icon all we need to do is :-

现在如图所示,我们必须展开三个阴影根元素以获得我们的搜索图标。要点击图标,我们需要做的就是:-

  [TestMethod]
        public void test()
        {
           IWebElement searchButton= getUIObject(By.CssSelector("downloads-manager"),By.CssSelector("downloads-toolbar"),By.Id("search-input"),By.Id("search-buton"));
            searchButton.Click();
        }

So just one line will give you your Web Element, just need to make sure you pass first shadow root element as first argument of the function "getUIObject" second shadow root element as second argument of the function and so on, finally last argument for the function will be the identifier for your actual element(for this case its 'search-button')

所以只有一行会给你你的 Web 元素,只需要确保你将第一个影子根元素作为函数“getUIObject”的第一个参数传递,第二个影子根元素作为函数的第二个参数,依此类推,最后是function 将是您实际元素的标识符(在这种情况下,它的'search-button'

回答by Maksym Barvinskyi

Until Selenium supports shadow DOM out of the box, you can try the following workaround in Java. Create a class that extends Byclass:

在 Selenium 支持开箱即用的 shadow DOM 之前,您可以在 Java 中尝试以下解决方法。创建一个扩展By类的类:

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.internal.FindsByCssSelector;

import java.io.Serializable;
import java.util.List;

public class ByShadow {
    public static By css(String selector) {
        return new ByShadowCss(selector);
    }

    public static class ByShadowCss extends By implements Serializable {

        private static final long serialVersionUID = -1230258723099459239L;

        private final String cssSelector;

        public ByShadowCss(String cssSelector) {
            if (cssSelector == null) {
                throw new IllegalArgumentException("Cannot find elements when the selector is null");
            }
            this.cssSelector = cssSelector;
        }

        @Override
        public WebElement findElement(SearchContext context) {
            if (context instanceof FindsByCssSelector) {
                JavascriptExecutor jsExecutor;
                if (context instanceof JavascriptExecutor) {
                    jsExecutor = (JavascriptExecutor) context;
                } else {
                    jsExecutor = (JavascriptExecutor) ((WrapsDriver) context).getWrappedDriver();
                }
                String[] subSelectors = cssSelector.split(">>>");
                FindsByCssSelector currentContext = (FindsByCssSelector) context;
                WebElement result = null;
                for (String subSelector : subSelectors) {
                    result = currentContext.findElementByCssSelector(subSelector);
                    currentContext = (FindsByCssSelector) jsExecutor.executeScript("return arguments[0].shadowRoot", result);
                }
                return result;
            }

            throw new WebDriverException(
                    "Driver does not support finding an element by selector: " + cssSelector);
        }

        @Override
        public List<WebElement> findElements(SearchContext context) {
            if (context instanceof FindsByCssSelector) {
                JavascriptExecutor jsExecutor;
                if (context instanceof JavascriptExecutor) {
                    jsExecutor = (JavascriptExecutor) context;
                } else {
                    jsExecutor = (JavascriptExecutor) ((WrapsDriver) context).getWrappedDriver();
                }
                String[] subSelectors = cssSelector.split(">>>");
                FindsByCssSelector currentContext = (FindsByCssSelector) context;
                for (int i = 0; i < subSelectors.length - 1; i++) {
                    WebElement nextRoot = currentContext.findElementByCssSelector(subSelectors[i]);
                    currentContext = (FindsByCssSelector) jsExecutor.executeScript("return arguments[0].shadowRoot", nextRoot);
                }
                return currentContext.findElementsByCssSelector(subSelectors[subSelectors.length - 1]);
            }

            throw new WebDriverException(
                    "Driver does not support finding elements by selector: " + cssSelector);
        }

        @Override
        public String toString() {
            return "By.cssSelector: " + cssSelector;
        }
    }
}

And you can use it without writing any additional functions or wrappers. This should work with any kind of framework. For example, in pure Selenium code this would look like this:

而且您无需编写任何额外的函数或包装器就可以使用它。这应该适用于任何类型的框架。例如,在纯 Selenium 代码中,这将如下所示:

WebElement searchButton =
    driver.findElement(ByShadow.css(
        "downloads-manager >>> downloads-toolbar >>> cr-search-field >>> #search-button"));

or if you use Selenide:

或者如果您使用硒化物:

SelenideElement searchButton =
    $(ByShadow.css("downloads-manager >>> downloads-toolbar >>> cr-search-field >>> #search-button"));

回答by odinho - Velmont

Normally you'd do this:

通常你会这样做:

element = webdriver.find_element_by_css_selector(
    'my-custom-element /deep/ .this-is-inside-my-custom-element')

And hopefully that'll continue to work.

希望这将继续有效。



However, note that /deep/and ::shadoware deprecated(and not implemented in browsers other than Opera and Chrome). There's much talk about allowing them in the static profile. Meaning, querying for them will work, but not styling.

但是,请注意/deep/::shadow弃用(并且未在 Opera 和 Chrome 以外的浏览器中实现)。有很多关于在静态配置文件中允许它们的讨论。意思是,查询它们会起作用,但不能设置样式。

If don't want to rely on /deep/or ::shadowbecause their futures are a bit uncertain, or because you want to work better cross-browser or because you hate deprecation warnings, rejoice as there's another way:

如果不想依赖/deep/或者::shadow因为他们的未来有点不确定,或者因为你想更好地跨浏览器工作,或者因为你讨厌弃用警告,请为另一种方式感到高兴:

# Get the shadowRoot of the element you want to intrude in on,
# and then use that as your root selector.
shadow_root = webdriver.execute_script('''
    return document.querySelector(
        'my-custom-element').shadowRoot;
    ''')
element = shadow_root.find_element_by_css_selector(
    '.this-is-inside-my-custom-element')

More about this:

更多关于这个:

回答by jeremysklarsky

This worked for me (using selenium javascript bindings):

这对我有用(使用 selenium javascript 绑定):

driver.executeScript("return $('body /deep/ <#selector>')")

That returns the element(s) you're looking for.

这将返回您正在寻找的元素。

回答by Arnab Das

For getting the filename of the latest downloaded file in Chrome

用于在 Chrome 中获取最新下载文件的文件名

def get_downloaded_file(self):
  filename = self._driver.execute_script("return document.querySelector('downloads-manager').shadowRoot.querySelector('#downloadsList downloads-item').shadowRoot.querySelector('div#content  #file-link').text")
  return filename

Usage:

用法:

driver.get_url('chrome://downloads')
filename = driver.get_downloaded_file()

And for configuring the option for setting the default download directory in selenium for chrome browser, where the corresponding file could be gotten:

以及配置Chrome浏览器selenium默认下载目录的选项,可以得到对应的文件:

..
chrome_options = webdriver.ChromeOptions()
..
prefs = {'download.default_directory': '/desired-path-to-directory'} # unix
chrome_options.add_experimental_option('prefs', prefs)
..