当Selenium遇到Shadow DOM,我只想砸键盘:一个老Tester的踩坑反思

🔑 关键词:Selenium Shadow DOM, Selenium和Playwright对比, Selenium慢, 自动化测试框架选型, WebDriver问题

📖 摘要:从一次真实的生产环境踩坑出发,聊Selenium的底层设计缺陷、等待机制的骗局,以及在2025年什么情况下还该继续用Selenium。不吹不黑,全是实际测试中的个人体验。

先交代背景:我在一家做企业级SaaS的公司干了三年测试开发,团队从10个人膨胀到40,UI自动化用例从800条跑到3000多。去年夏天,一个核心模块重构后引入了大量Web Components,我们用了半个月去修Selenium脚本,不是等不到元素就是点击无效。最崩溃的一次,某个按钮藏在三层iframes + 开放Shadow DOM里,我试了WebDriverWait、ActionChains、JSExecutor滚动、甚至Force click,最后没办法,让前端加了个data-testid并改成closed shadow mode——就是这周,我决定把剩下所有关键链路切到Playwright,但不是因为Playwright有多神,而是Selenium让你在一个本该20行的脚本里,写一堆胶水代码来伺候浏览器的神经病行为。

图片

先说说Selenium慢这个老生常谈的问题。很多人归咎于网络、等待策略,但真正坑的是WebDriver线协议是阻塞式请求-响应,每次findElement像一次API调用,一个click再来一次,如果页面里有React的异步刷新,Selenium常常拿到的还是旧引用,然后StaleElementReferenceException就像定时炸弹。我们开过监控,脚本平均每步操作耗时850ms,同样流程在Playwright的CDP协议下只要320ms。有人说可以用Selenium Grid并行来补,但你也得维护Grid,你还要自己处理Selenium Manager带来的驱动版本地狱。Chrome 115之后有个怪事,Selenium Manager自动帮我们下载了115的chromedriver,但服务器上Chrome还是114,结果生产流水线挂了半天,真是个笑话。

图片

Shadow DOM的问题更是Selenium的硬伤。CSS选择器完全不穿透,只有用JS的querySelector或者自己封装Locator。但哪怕你用execute_script返回的是WebElement,你让selenium去click,它也得先把元素拖到可交互区域,而Shadow宿主本身的布局经常导致元素可见但Selenium说not clickable。一个解决方法是你查到Selenium官方说支持Shadow DOM是4.0以后的,但实际用下来它只是帮你拿到shadowRoot,后续操作还要你自己调ShadyDom polyfill?别天真了。我们最后写了大概60行的工具类专门处理shadow dom定位:先从document取得shadowRoot,再遍历内部find,每一步都要套try-catch,还要考虑iframe切换。这不是写脚本,这是考古。

图片

有同事说,你可以用Selenium IDE录制一下省点事。我笑了,IDE在2024年就跟死了一样,Chrome扩展一更新就废,录出来的脚本连id=u_123_456这种动态id都处理不了。Selenium最大的问题不是功能缺失,而是设计理念还停留在2011年——WebDriver规范被W3C定死了,浏览器实现也各有各的坑。你知道吗,Safari的SafariDriver会在打开新窗口时返回空句柄概率约17%,没人修。Firefox的geckodriver在Windows上偶尔启动时会cpu飙到100%,我们得在外部写个看门狗杀进程重跑。但Selenium的社区生态和StackOverflow回答是真的多,你踩过的坑基本都有人踩过,这对初学者是友好的,可是反过来也让很多团队误以为“有坑是正常的”。

图片

换个角度说,Selenium绝对有不可替代的领域。比如如果你需要同时测Chrome、Firefox、Safari和旧版Edge,而且你的CI环境连不了外网,只允许局域网安装包,那Selenium的灵活性和语言绑定优势就出来了。Playwright虽好,但它官方只帮你管Chromium/Firefox/WebKit的构建,对Safari魔改版的支持你得用他们提供的WebKit,你要是想直接驱动本机Safari就别想了,Apple的bug让你有得折腾。另外,如果你是写低代码平台的数据爬虫,用Selenium的page_source + requests反而更顺,因为你可以动态提取cookies并复用会话,而不需要启动CDP那种重型通道。说白了,选型不是“哪个好”,而是“你的测试是面向业务流程,还是面向浏览器怪胎”。

图片

最后说说等待。Selenium的隐式等待和显式等待混在一起是个坑,坑到什么程度?你设了implicitly_wait(10)然后又用WebDriverWait,在某些版本里,隐式等待会在每次find_element时生效,导致你显式条件检查一个元素不存在时,它先磨蹭10秒再给你抛异常。我们线上脚本失败截图里,超过一半是NoSuchElementException但手动用devtools查明明就在页面里——因为它是shadow content或iframe中还未激活的渲染节点。Selenium的做法太静态,而现代SPA页面的DOM是活着的,一个元素的“存在”不代表“可访问”。Playwright的auto-wait是真的把可见性、稳定性和接收事件都内建了,这种默认的务实行为,正是Selenium无论怎么封装都追不上的。

图片

其实我写这篇并没有打算捧一踩一,我只是受够了“用框架装饰旧技术”的瞎折腾。如果今天有人问我,我建议新项目直接用Playwright,除非你的团队只会Java和Kotlin,觉得语法迁移成本太高,那你就继续Selenium+增强版FindElement吧。但如果你跟我一样曾经为了一个该死的button来回调JS和加Sleep,你应该知道我在说什么:真正要解决的不是“等多久”,而是“你确定等到了能交互的那一瞬间吗?”Selenium没有回答这个问题,它给了你一把key,却让你自己去开门锁找门在后面。这就是我的结论:Selenium没死,但它该退休了,而Playwright是另一回事,不是接班,是换了个游戏。