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 的可达性检查 |
六、建议与扩展
- 动态注入劫持脚本:可封装成通用函数,在每次进入新的 shadow DOM 前自动执行。
- 封装工具类简化操作:将 shadow root 查找和劫持 parentNode 的逻辑封装成工具函数,提高复用性。
- 等待机制优化:结合 WebDriverWait 显式等待 shadow DOM 加载完成,避免查找失败。
- 兼容性注意:不同版本的 ChromeDriver / Selenium 行为可能略有差异,建议统一版本环境。
七、参考资料
- Chromium Source - isNodeReachable
- Selenium Documentation
- Shadow DOM and Selenium – Issues and Workarounds
如果你在使用 Selenium 自动化 Web Component 或无界微前端时也遇到了类似问题,希望这篇文章能为你提供思路和帮助。欢迎留言讨论更多 Shadow DOM 自动化的实践经验!💡
评论
0理性交流 · 友善评论