如何从 Selenium 处理 Shadow DOM 中的元素

Posted

技术标签:

【中文标题】如何从 Selenium 处理 Shadow DOM 中的元素【英文标题】:How to handle elements inside Shadow DOM from Selenium 【发布时间】:2016-09-19 22:45:44 【问题描述】:

我想在chromedriver 中自动检查文件下载完成。 下载列表中每个条目的html 看起来像

<a is="action-link" id="file-link" tabindex="0" role="link" href="http://fileSource" class="">DownloadedFile#1</a>

所以我使用以下代码来查找目标元素:

driver.get('chrome://downloads/')  # This page should be available for everyone who use Chrome browser
driver.find_elements_by_tag_name('a')

当有 3 个新下载时,这将返回空列表。

我发现,只能处理#shadow-root (open) 标签的父元素。 那么如何在这个#shadow-root 元素中找到元素呢?

【问题讨论】:

driver.find_elements_by_id("file-link") 有帮助吗? 没有。这将返回相同的空列表 好的,那么 Css/Xpath 可能仍然作为访问driver.find_elements_by_css_selector(".[id='file-link']") 的方式为您提供了一些价值? 你的语句返回InvalidSelectorExceptiondriver.find_elements_by_css_selector("[id='file-link']")返回空列表 @Anderson : 你错过了driver.find_elements_by_css_selector(".[id='file-link']") 后面的. 吗? 【参考方案1】:

您可以使用driver.executeScript() 方法访问网页中的 HTML 元素和 javascript 对象。

在下面的示例中,executeScript 将在 Promise 中返回存在于元素影子树中的所有 &lt;a&gt; 元素的节点列表,其中 idhost。然后你可以执行你的断言测试:

it( 'check shadow root content', function () 

    return driver.executeScript( function ()
    
        return host.shadowRoot.querySelectorAll( 'a' ).then( function ( n ) 
        
            return expect( n ).to.have.length( 3 )
        
     )
 )     

注意:我不懂 Python,所以我使用了 JavaScript 语法,但它应该以同样的方式工作。

【讨论】:

我不知道这段代码是什么意思 :) 而且我从未在JS 中看到过=&gt; 符号它的用途是什么?...任何人都可以“翻译”这段代码吗? () => 是一个 lambda 表达式/内联函数语法。我更新了我的答案以使用标准函数声明。【参考方案2】:

有时影子根元素是嵌套的,第二个影子根在文档根中不可见,但在其父访问的影子根中可用。我认为最好使用 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()

为了说明这一点,我刚刚在 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()

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

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()

稍后编辑:

我最近尝试访问内容设置(请参阅下面的代码),它有多个影子根元素重叠,现在如果不先扩展另一个,您将无法访问其中一个,而您通常还有动态内容和 3 个以上的影子元素一个进入另一个它使自动化成为不可能。上面的答案在前几次使用过,但足以让一个元素改变位置,并且您需要始终使用检查元素并在树上查看它是否在阴影根中,自动化噩梦。

当您发现按钮此时不可点击时,不仅很难找到内容设置,因为 shadowroot 和动态变化。

driver = webdriver.Chrome()


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

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

root2 = shadow_root1.find_element_by_css_selector('[page-name="Settings"]')
shadow_root2 = expand_shadow_element(root2)

root3 = shadow_root2.find_element_by_id('search')
shadow_root3 = expand_shadow_element(root3)

search_button = shadow_root3.find_element_by_id("searchTerm")
search_button.click()

text_area = shadow_root3.find_element_by_id('searchInput')
text_area.send_keys("content settings")

root0 = shadow_root1.find_element_by_id('main')
shadow_root0_s = expand_shadow_element(root0)


root1_p = shadow_root0_s.find_element_by_css_selector('settings-basic-page')
shadow_root1_p = expand_shadow_element(root1_p)


root1_s = shadow_root1_p.find_element_by_css_selector('settings-privacy-page')
shadow_root1_s = expand_shadow_element(root1_s)

content_settings_div = shadow_root1_s.find_element_by_css_selector('#site-settings-subpage-trigger')
content_settings = content_settings_div.find_element_by_css_selector("button")
content_settings.click()

【讨论】:

嗨,爱德华,我迟到了。我尝试使用您的代码,但似乎 shadow_root1 没有 find_element_by_whatever 方法。我做错什么了吗?基本上我有root1 = driver.find_element_by_tag_name('input'),然后是shadowRoot1 = ExpandShadowElement(root1) 他们一直在更改它,没有时间查看和更新​​ 啊,谢谢!其实我发现我不需要解析shadow DOM,设法登录而不接触它们,不知道为什么......【参考方案3】:

我会将此添加为评论,但我没有足够的声望点--

Eduard Florinescu 的回答很好地说明了一旦你在 shadowRoot 中,你只有与可用的 JS 方法相对应的 selenium 方法——主要是通过 id 选择。

为了解决这个问题,我在 python 字符串中编写了一个更长的 JS 函数,并使用原生 JS 方法和属性(通过 id、children + 索引等查找)来获取我最终需要的元素。

使用 driver.execute_script() 运行 JS 字符串时,您还可以使用此方法访问子元素的 shadowRoots 等

【讨论】:

【参考方案4】:

还有可以使用的pyshadow pip 模块,在我的情况下可以使用,下面的例子:

from pyshadow.main import Shadow
from selenium import webdriver

driver = webdriver.Chrome('chromedriver.exe')
shadow = Shadow(driver)
element = shadow.find_element("#Selector_level1")
element1 = shadow.find_element("#Selector_level2")
element2 = shadow.find_element("#Selector_level3")
element3 = shadow.find_element("#Selector_level4")
element4 = shadow.find_element("#Selector_level5")
element5 = shadow.find_element('#control-button') #target selector
element5.click() 

【讨论】:

【参考方案5】:

为了简单起见,我最初将 Eduard 的解决方案稍微修改为一个循环。但是当 Chrome 更新到 96.0.4664.45 时 selenium 在调用 'return arguments[0].shadowRoot' 时开始返回 dict 而不是 WebElement。

我做了一些修改,发现我可以通过调用 return arguments[0].shadowRoot.querySelector("tag") 让 Selenium 返回一个 WebElement。

这是我的最终解决方案最终的样子:

def get_balance_element(self):
        # Loop through nested shadow root tags
        tags = [
            "tag2",
            "tag3",
            "tag4",
            "tag5",
            ]

        root = self.driver.find_element_by_tag_name("tag1")

        for tag in tags:
            root = self.expand_shadow_element(root, tag)

        # Finally there.  GOLD!

        return [root]

def expand_shadow_element(self, element, tag):
    shadow_root = self.driver.execute_script(
        f'return arguments[0].shadowRoot.querySelector("tag")', element)
    return shadow_root

干净简单,适合我。

另外,我只能得到这个工作的 Selenium 3.141.0。 4.1 有一个半生不熟的影子 DOM 实现,它只是设法破坏一切。

【讨论】:

Chrome 96+ 旨在使用 Python Selenium 4.1 中的新 shadow_dom 属性。我在这里也有 Selenium 3 的 hack:titusfortner.com/2021/11/22/shadow-dom-selenium.html

以上是关于如何从 Selenium 处理 Shadow DOM 中的元素的主要内容,如果未能解决你的问题,请参考以下文章

java+selenium,请问该如何定位#shadow-root里面的元素?

如何使用 Python 在 selenium 中编辑#shadow-root(用户代理)值

如何使用 jquery 和 selenium 在“chrome://downloads”访问“shadow-root”下的元素?

有人知道如何使用 selenium webdriver 识别 shadow dom web 元素吗?

如何使用 Selenium Java 框架自动化 Shadow DOM

如何使用量角器或selenium在网页中的Shadow-root下的文本框中输入文本。