跳到主要内容
←返回
/修订于

Selenium 获取 Shadow DOM 内部元素时出现 `StaleElementReferenceException` 的问题排查与解决

在使用 Selenium 操控浏览器自动化测试无界微前端(Wujie)应用的过程中,我们遇到了一个奇特的问题:无法通过常规方式获取 Shadow DOM 中的元素,抛出 StaleElementReferenceException 异常。本文将详细记录这次问题的排查过程、分析成因,并提供有效的解决方案。


一、问题现象

我们在尝试通过 Selenium 获取某个自定义 Web Component(如 <wujie-app>)内部 Shadow DOM 中的 <input> 元素时,出现了如下异常:

StaleElementReferenceException: Message: stale element reference: stale element not found

代码示例如下:

chrome = Chrome()
chrome.get(url)

host = chrome.find_element("css selector", "wujie-app")
shadow_root = host.shadow_root
element = shadow_root.find_element("css selector", "input")  # 抛出异常

同样的逻辑,使用 JavaScript 在浏览器控制台中执行却能正常获取:

document.querySelector("wujie-app").shadowRoot.querySelector("input")
// 成功返回 input 元素

这说明问题并非网页本身做了限制或保护机制,而是 Selenium 或 ChromeDriver 内部处理 shadow DOM 的方式上存在问题。


二、进一步验证尝试

尝试2:通过 execute_script 执行 JS 获取元素

element = chrome.execute_script(
    "return arguments[0].shadowRoot.querySelector('input')", host
)
# 仍然报错:StaleElementReferenceException

奇怪的是,虽然 JavaScript 能成功返回元素,但一旦试图将其返回给 Python 端作为 WebElement 使用,就会抛出异常。

尝试3:控制台直接执行 JS 获取元素

document.querySelector("wujie-app").shadowRoot.querySelector("input")
// 正确返回 <input> 元素

尝试4:获取属性而非元素对象

classList = chrome.execute_script(
    "return arguments[0].shadowRoot.querySelector('div.finder-page.PostCreate').classList",
    host,
)
# 返回 ['finder-page', 'PostCreate'],成功!

这表明:JavaScript 能正确访问并操作 Shadow DOM 中的内容,但是当尝试从 JS 返回一个 Element 对象到 Selenium 时,会触发 StaleElementReferenceException。


三、深入原因分析

查阅相关资料和 Chromium / Selenium 源码后发现,ChromeDriver 在处理返回的 DOM 元素时,会对节点进行可达性检查(isNodeReachable)。

具体来说,这个函数会比较当前节点的 ownerDocument 和全局的 document 是否一致。但在 Shadow DOM 中,shadowRoot 下的节点其所属文档是 Shadow Document Fragment,而不是全局 Document。因此,在某些情况下,ChromeDriver 判断这些节点不可达,从而导致该异常。


四、解决方案

既然问题出在 ChromeDriver 的节点可达性判断逻辑上,我们可以通过修改 Shadow DOM 中节点的属性来绕过这一限制。

✅ 最终解决方案

在访问 Shadow DOM 元素前,运行以下 JavaScript 代码,篡改目标节点的 parentNode 属性,使其指向全局 document,以欺骗 ChromeDriver 的可达性检查:

chrome.execute_script("""
    const host = document.querySelector('wujie-app');
    const shadowRoot = host.shadowRoot;
    if (shadowRoot) {
        const firstChild = shadowRoot.firstElementChild;
        Object.defineProperty(firstChild, "parentNode", {
            enumerable: true,
            configurable: true,
            get: () => document,
        });
    }
""")

这段代码的作用是:

  • 获取 <wujie-app> 的 shadow root;
  • 修改 shadow root 中第一个子元素的 parentNode 属性为全局 document;
  • 使得 ChromeDriver 在进行节点可达性检查时认为该节点“合法”。

此时再进行查找即可成功获取元素:

host = chrome.find_element("css selector", "wujie-app")
shadow_root = host.shadow_root
element = shadow_root.find_element("css selector", "input")
element.send_keys("Hello World")

五、总结

方法 是否成功 原因
直接通过 shadow_root.find_element() ❌ ChromeDriver 可达性检查失败
通过 execute_script 获取元素 ❌ 同样受制于节点可达性检查
控制台执行 JS 获取元素 ✅ 不涉及 WebDriver 的节点检测
获取元素属性值(如 classList) ✅ 仅传递字符串,不涉及节点引用
修改 parentNode 劫持至全局 document ✅ 绕过 ChromeDriver 的可达性检查

六、建议与扩展

  1. 动态注入劫持脚本:可封装成通用函数,在每次进入新的 shadow DOM 前自动执行。
  2. 封装工具类简化操作:将 shadow root 查找和劫持 parentNode 的逻辑封装成工具函数,提高复用性。
  3. 等待机制优化:结合 WebDriverWait 显式等待 shadow DOM 加载完成,避免查找失败。
  4. 兼容性注意:不同版本的 ChromeDriver / Selenium 行为可能略有差异,建议统一版本环境。

七、参考资料


如果你在使用 Selenium 自动化 Web Component 或无界微前端时也遇到了类似问题,希望这篇文章能为你提供思路和帮助。欢迎留言讨论更多 Shadow DOM 自动化的实践经验!💡

评论

0

0/500