【问题标题】:How to handle elements inside Shadow DOM from Selenium如何从 Selenium 处理 Shadow DOM 中的元素
【发布时间】: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']") 后面的. 吗?

标签: python selenium selenium-webdriver shadow-dom


【解决方案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 实现,它只是设法破坏一切。

      【讨论】:

      猜你喜欢
      • 2020-01-30
      • 1970-01-01
      • 2019-11-13
      • 1970-01-01
      • 2021-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多