Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

 

在我最近的一个自动化项目中,我正在编写代码以单击网页上的某个元素。这是一个带有 id 的非常简单的元素avatar。令我惊讶的是,Selenium 未能找到该元素并抛出异常NoSuchElementException。我更仔细地检查了那个元素,发现这个元素在一些奇怪的元素里面shadow-root。事实上,有一个元素树,包括avatar在那个元素里面shadow-root

一个快速的谷歌搜索显示这shadow-root不是一个常规的 DOM 元素,它是影子 DOM 的一部分。到目前为止,Selenium WebDriver 无法与之交互。w3c有一个提案正在等待中来支持它。

 

什么是 DOM?

在了解 shadow DOM 之前,您应该首先熟悉 DOM。

文档对象模型 ( DOM )通过 在内存中表示文档的结构(例如表示网页的 HTML)将网页连接到脚本或编程语言

https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model#HTML_DOM

 

简而言之,当 Web 浏览器获取 HTML 页面时,它会解析文档并将其转换为加载到内存中的 DOM。这是一个非常简单的 HTML 文档:

<html lang="en">
 <head>
   <title>A simple web page</title>
  </head>
 <body>
    <h1>Hello world</h1>
    <p>I am rendered!</p>
  </body>
</html>

 

该文档的 HTML DOM 表示形式如下所示:

Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?
Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

 

如您所见,DOM 是一个树状结构。请随时阅读本文中有关 DOM 的更多信息。

 

什么是shadow DOM?

shadow DOM 是一种在 HTML 文档中实现封装的方法。通过实现它,您可以隐藏文档一部分的样式和行为,并与同一文档的其他代码分开,这样就不会受到干扰。

Shadow  DOM 允许将隐藏的 DOM 树附加到常规 DOM 树中的元素——这个 shadow DOM 树从一个 shadow root 开始,在它的下面可以附加到任何你想要的元素,就像普通 DOM 一样。

Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?
Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

有一些 shadow DOM 术语需要注意:

  • Shadow host:shadow DOM 附加到的常规 DOM 节点。
  • Shadow tree: shadow DOM 中的 DOM tree。
  • Shadow boundary: shadow DOM 结束的地方,常规 DOM 开始的地方。
  • Shadow root: Shadow tree的根节点。

以上部分摘自MDN您可以在此处阅读有关 shadow DOM的更多信息。

 

如何访问Shadow DOM?

您可以通过在主页面上下文中运行常规 JavaScript(如果其模式为open.

让我们看下面的 HTML 代码片段:

<div>
  <div id="shell">
    #shadow-root (open)
      <div id="avatar"></div>
  </div>
  <a href="./logout.html">Logout</a>
</div>

 

如您所见,它既有影子 DOM(avatar)也有常规 DOM 元素(Logout链接)。

要使用 JavaScript 访问影子 DOM 元素,您首先需要查询 shadow host 元素,然后才能访问其shadowRoot属性。一旦您可以访问shadowRoot,您就可以像常规 JavaScript 一样查询 DOM 的其余部分。

var host = document.getElementById('shell');
var root = host.shadowRoot;
var avatar = root.getElementById('avatar');

 

如何编写 Selenium 代码来访问 shadow DOM?

 

我们可以利用该特性,将一段 JavaScript 注入浏览器,以获取 shadow DOM 中的目标元素。一旦我们有了目标元素,我们就可以将其解析为 aWebElement并且可以对该元素执行任何有效的操作。

 

Java 代码:

WebElement host = driver.findElement(By.id("shell"));
JavascriptExecutor js = (JavascriptExecutor)driver;
WebElement shadowRoot = (WebElement)(js.executeScript("return arguments[0].shadowRoot", host));
shadowRoot.findElement(By.id("avatar")).click();

 

Python 代码:

host = driver.find_element_by_id("shell"))
shadowRoot = driver.execute_script("return arguments[0].shadowRoot", host)
shadowRoot.find_elemen_by.id("avatar")).click()

 

有时我观察到 WebDriver 的click()方法会抛出一些异常。在这种情况下,我们可以直接使用 JavaScript 的click()方法。

 

Java 代码:

WebElement host = driver.findElement(By.id("shell"));
JavascriptExecutor js = (JavascriptExecutor)driver;
js.executeScript("arguments[0].shadowRoot.getElementById('avatar').click()", host);

 

Python 代码:

host = driver.find_element_by_id("shell"))
driver.execute_script("return arguments[0].shadowRoot.getElementById('avatar').click()", host)

 

嵌套的shadow DOM

有时会有一个更复杂的 DOM 结构,其中我们有多个相互嵌套的shadow DOM。如果您检查 Chrome 浏览器的下载页面 – chrome://downloads/,您会发现以下 DOM 结构:

Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?
Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

如您所见,shadow DOM 有三层相互嵌套。如果您想访问<div id="leftContent">位于第三个影子 DOM 内的目标元素怎么办?

好吧,您可以应用我们目前在本教程中学到的相同原则——编写 JavaScript 以首先访问shadow host,然后通过访问shadowRoothost上的属性来获取shadow DOM。一旦您可以访问第一个 shadow DOM,您就可以遍历它并尝试访问第二个 shadow DOM 的根,依此类推。

document.getElementsByTagName('downloads-manager')[0]
.shadowRoot
.getElementById('toolbar')
.shadowRoot
.getElementById('toolbar')
.shadowRoot
.getElementById('leftContent')

 

如果我们想点击那个元素,我们可以将完整的 JavaScript 注入浏览器:

 

Java 代码:

WebElement host = driver.findElement(By.tagName("downloads-manager"));
JavascriptExecutor js = (JavascriptExecutor)driver;
js.executeScript("arguments[0].shadowRoot.getElementById('toolbar').shadowRoot.getElementById('toolbar').shadowRoot.getElementById('leftContent').click()", host);

 

Python 代码:

firstHost = driver.find_element_by_tag_name("downloads-manager")
driver.execute_script("return arguments[0].shadowRoot.getElementById('toolbar').shadowRoot.getElementById('toolbar').shadowRoot.getElementById('leftContent').click()", host)

 

或者:

WebDriverWait(self.driver, 5, 0.5).until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'kat-tab[tab-id="DATE_RANGE_REPORTS"]')))
script = 'document.querySelector("kat-tab[tab-id=DATE_RANGE_REPORTS]").querySelector("span[slot=label]").click();'
self.driver.execute_script(script)

 

我们可以优化上面的代码,写一个可以返回任何shadow host的shadow DOM 的辅助方法:

 

Java 代码:

public WebElement getShadowRoot(WebElement host) {
  JavascriptExecutor js = (JavascriptExecutor)driver;
  WebElement shadowRoot = (WebElement) js.executeScript("return arguments[0].shadowRoot", host);
  return shadowRoot;
}

 

Python 代码:

def getShadowRoot(host):
    shadowRoot = driver.executeScript("return arguments[0].shadowRoot", host)
    return shadowRoot

 

每次需要访问 shadow DOM 时,我们都可以使用这个辅助方法:

 

Java 代码:

//Get first shadow host and access its shadow root
WebElement host1 = driver.findElement(By.tagName("downloads-manager"));
WebElement root1 = getShadowRoot(host1);
//Get second shadow host and access its shadow root
WebElement host2 = shadowRoot1.findElement(By.id("toolbar"));
WebElement root2 = getShadowRoot(host2);
//Get third shadow host and access its shadow root
WebElement host2 = shadowRoot2.findElement(By.id("toolbar"));
WebElement root3 = expandRootElement(host2);
//Get the target element inside the third shadow DOM
WebElement downloads = root3.findElement(By.id("leftContent")).getText();
assert downloads.getText().contains('Downloads');

 

Python 代码:

# Get first shadow host and access its shadow root
host1 = driver.find_element_by_tag_name("downloads-manager")
root1 = getShadowRoot(host1)
# Get second shadow host and access its shadow root
host2 = shadowRoot1.find_element_by_id("toolbar")
root2 = getShadowRoot(host2)
# Get third shadow host and access its shadow root
host2 = shadowRoot2.find_element_by_id("toolbar")
root3 = expandRootElement(host2)
# Get the target element inside the third shadow DOM
downloads = root3.find_element_by_id("leftContent")
assert 'Downloads' in downloads.text

 

挑战

Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?
Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

您可以应用本教程的学习内容并在 Chrome 下载页面的搜索栏中输入一些文本吗?如果您检查搜索栏并仔细观察,您会发现它位于嵌套的第三个影子 DOM 内。请随时寻求帮助或在下面的评论中发布您的解决方案。

 

 

本文:Selenium 抓取Shadow Dom,selenium-shadowDOM节点操作, Selenium 操作 shadow DOM, How to interact with shadow DOM in Selenium?

 

 

 

Add a Comment

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload CAPTCHA.