使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一)
文章目录
- 使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一)
- 一、前言:为什么 Selenium 自动化里“看不到” WebSocket?
- 二、整体技术方案:Selenium 驱动,Playwright 监听
- 2.1 方案结构概览
- 2.2 为什么选择 Playwright?
- 三、环境与前提条件说明
- 3.1 技术栈前提
- 3.2 重要约束条件
- 四、第一步:让 Selenium 启动一个“可被监听”的浏览器
- 4.1 为什么需要 remote-debugging-port?
- 4.2 注入 ChromeOptions 的关键时机
- 4.3 在 TestBase 中注入 remote-debugging-port
- 五、代码实战:实现 PlaywrightWsCollector(专职监听 WebSocket)
- 5.1 设计一个可复用的消息模型 WsMessage
- 5.2 端口“桥接”对象(让 TestBase 能拿到端口)
- 5.3 PlaywrightWsCollector:核心类骨架
- 5.3.1 生成空闲端口(多机/多进程安全)
- 5.4 完整实现:PlaywrightWsCollector.cs
- 5.5 如何在测试类里“正确初始化” Collector(时序写法)
- 5.5.1 ClassInitialize:生成端口 + 初始化 Collector,但不连接
- 5.5.2 TestInitialize:先 base,再 EnsureConnected
- 5.6 本节小结
- 下一篇预告
一、前言:为什么 Selenium 自动化里“看不到” WebSocket?
在现代前端应用中,越来越多的核心逻辑通过WebSocket完成,例如:
- 多人协同编辑(SelectionChange、EditRecord 等实时广播)
- 实时状态同步
- 前端操作与后端推送解耦
在浏览器中,这些信息可以通过
DevTools → Network → WebSocket → Messages直观查看。
但在 Selenium 自动化测试中,会遇到一个现实问题:
- Selenium 3.x没有 DevTools API
- Selenium 4.x 以上,如果使用了 DevTools API,需要持续更新WebDirver版本以匹配不断更新的浏览器
这在以下场景中会成为瓶颈:
- 需要验证某个操作是否真的向后端发送了
Action:"EditRecord" - 需要校验某一步操作触发了几次
SelectionChange - 仅靠 UI 状态不足以判断功能是否正确
本文将介绍一种借助Playwright辅助Selenium 框架的解决方案:
👉使用 Playwright 作为“旁路监听器”,专门监听 WebSocket。
二、整体技术方案:Selenium 驱动,Playwright 监听
本方案的核心思想是:
Selenium 继续负责“操作页面”,Playwright 只负责“监听浏览器的 WebSocket”。
两者并不是互相替代,而是各司其职。
2.1 方案结构概览
整体结构可以概括为:
Selenium
- 创建并管理 Chrome WebDriver
- 执行页面操作(点击、输入、拖拽等)
Chrome
- 启动时开启
--remote-debugging-port
- 启动时开启
Playwright
- 通过 Chrome DevTools Protocol(CDP)连接到同一个浏览器实例
- 监听 WebSocket 帧(FrameSent / FrameReceived)
测试代码
- 对捕获到的 WebSocket 消息进行解析和断言
2.2 为什么选择 Playwright?
Playwright 在这里的价值主要体现在:
- 原生支持CDP(Chrome DevTools Protocol)
- 可以附着到已经存在的浏览器进程
- 提供
page.WebSocket事件,直接获取 WS 帧数据 - 不影响 Selenium 的 UI 操作
这意味着:
我们不需要重写现有 Selenium 用例,只需额外“接一根线”。
三、环境与前提条件说明
在继续之前,需要明确本文适用的技术前提。
3.1 技术栈前提
本文基于以下环境设计:
Selenium WebDriver3.10.x
测试框架:MSTest
浏览器:Chrome
Playwright:.NET 官方版本
已存在的自动化框架中:
- 有统一的
TestBase - 有 Browser 封装(支持
SetChromeOptions)
- 有统一的
3.2 重要约束条件
这是本文方案成立的关键前提:
不能修改 Selenium Browser 核心实现
- 如
CreateWebDriver()是私有方法 - Browser 类可能来自 DLL
- 如
但可以:
- 修改
TestBase - 在测试类中增加辅助逻辑
- 修改
测试用例可能在:
- 多台机器
- 多进程
- 并发执行
因此,本方案必须满足:
- 对现有框架侵入极小
- 不引入端口冲突
- 不影响非 WebSocket 用例
四、第一步:让 Selenium 启动一个“可被监听”的浏览器
Playwright 想要监听 WebSocket,有一个前提条件:
浏览器必须开启 remote-debugging-port。
4.1 为什么需要 remote-debugging-port?
Playwright 并不是“魔法监听”,它的本质是:
- 通过 CDP(Chrome DevTools Protocol)
- 连接到浏览器调试端口
- 订阅 Network / WebSocket 事件
如果 Chrome 启动时没有开启该端口,Playwright 无从连接。
4.2 注入 ChromeOptions 的关键时机
在很多 Selenium 框架中:
- WebDriver 是懒加载的
- Browser 在第一次访问
WebDriver时才真正启动浏览器
幸运的是,大多数成熟框架都会提供一个生命周期钩子,例如:
protectedvirtualvoidAfterInitializeTestDriver()这个方法的特点是:
- Browser 已创建
- WebDriver 尚未创建
- 正是注入 ChromeOptions 的最佳时机
4.3 在 TestBase 中注入 remote-debugging-port
示例代码如下:
protectedoverridevoidAfterInitializeTestDriver(){base.AfterInitializeTestDriver();if(PlaywrightWsCollectorBridge.DebugPort==0)return;varbrowser=this.ActiveBrowser;if(browser.BrowserType==BrowserType.Chrome){varoptions=newChromeOptions();options.AddArgument($"--remote-debugging-port={PlaywrightWsCollectorBridge.DebugPort}");browser.SetChromeOptions(options);}}这里做了三件关键的事:
- 仅在需要 WebSocket 的测试中生效(通过 DebugPort 判断)
- 使用 Chrome 原生支持的
--remote-debugging-port - 不修改 Browser 内部实现,只调用公开扩展点
到这里,Selenium 启动的 Chrome 已经具备了“被 Playwright 监听”的能力。
五、代码实战:实现 PlaywrightWsCollector(专职监听 WebSocket)
这一节的目标很明确:把“DevTools 里 WS → Messages 面板看到的东西”,变成测试代码里可读取、可切片、可断言的数据结构。
同时要满足几个工程要求:
- Playwright不接管 UI,只监听;
- 连接必须通过CDP 端口,而端口是 Selenium 启动 Chrome 时注入的;
- 避免常见坑:
.NET 没有 IBrowser.ContextCreated、ECONNREFUSED等; - 能做到“只取某一步操作产生的 WS 消息”,而不是全程污染。
5.1 设计一个可复用的消息模型 WsMessage
我们不仅要拿到文本,还要知道它属于:
- 上箭头 / 下箭头(Sent / Received)
- 以及消息发生顺序(用于“操作切片”)
publicclassWsMessage{publiclongSeq{get;set;}// 递增序号:用于切片publicstringMessageType{get;set;}// "Sent" / "Received"publicstringText{get;set;}// WebSocket 帧内容(一般是 JSON)}5.2 端口“桥接”对象(让 TestBase 能拿到端口)
在第 4 节里已经在TestBase.AfterInitializeTestDriver()注入了:
--remote-debugging-port=xxx但TestBase自己并不知道端口是什么,因此我们用一个小桥接类存放端口值即可:
publicstaticclassPlaywrightWsCollectorBridge{publicstaticintDebugPort{get;set;}=0;}约定:
DebugPort == 0:表示当前用例不需要 WS 监听;- 非 0:TestBase 注入该端口,Playwright 用同一个端口连接。
5.3 PlaywrightWsCollector:核心类骨架
这个类负责:
- 生成一个可用端口(供 Selenium 启动 Chrome)
- 在浏览器启动后,通过 CDP 连接
- hook page 的 WebSocket 事件
- 收集消息、切片、分类、清理
关键点:不要在构造函数里 Connect,否则浏览器还没启动就连,会直接ECONNREFUSED。
5.3.1 生成空闲端口(多机/多进程安全)
usingSystem.Net;usingSystem.Net.Sockets;publicstaticintGenerateFreePort(){varlistener=newTcpListener(IPAddress.Loopback,0);listener.Start();intport=((IPEndPoint)listener.LocalEndpoint).Port;listener.Stop();returnport;}5.4 完整实现:PlaywrightWsCollector.cs
说明:这里使用
127.0.0.1,避免某些机器localhost解析到 IPv6::1导致连接异常。
usingMicrosoft.Playwright;usingSystem.Collections.Concurrent;usingSystem.Threading;usingSystem.Linq;publicclassPlaywrightWsCollector:IDisposable{privateIPlaywright_playwright;privateIBrowser_browser;privatereadonlyConcurrentBag<WsMessage>_messages=newConcurrentBag<WsMessage>();privatelong_seq=0;privatebool_connected=false;privatebool_pageHooked=false;publicintDebugPort{get;}publicPlaywrightWsCollector(intdebugPort){DebugPort=debugPort;// 仅保存端口,不做连接}/// <summary>/// 在 Selenium 浏览器启动(带 remote-debugging-port)后调用。/// </summary>publicvoidEnsureConnected(){if(_connected)return;_playwright=Playwright.CreateAsync().GetAwaiter().GetResult();varurl=$"http://127.0.0.1:{DebugPort}";_browser=_playwright.Chromium.ConnectOverCDPAsync(url).GetAwaiter().GetResult();HookExistingContexts(_browser);_connected=true;}privatevoidHookExistingContexts(IBrowserbrowser){// .NET 里没有 IBrowser.ContextCreated,所以只能:// 1) 先 hook 现有 contextsforeach(varctxinbrowser.Contexts){HookContext(ctx);}}privatevoidHookContext(IBrowserContextctx){// hook 已有 pageforeach(varpageinctx.Pages){HookPage(page);}// hook 新打开的 page(例如 Selenium 新开 tab/window)ctx.Page+=(_,page)=>HookPage(page);}privatevoidHookPage(IPagepage){// 避免重复 hook:同一个测试通常只关心当前页面的 WSif(_pageHooked)return;page.WebSocket+=(sender,ws)=>{ws.FrameSent+=(s2,frame)=>{AddWs("Sent",frame.Text);};ws.FrameReceived+=(s3,frame)=>{AddWs("Received",frame.Text);};};_pageHooked=true;}privatevoidAddWs(stringtype,stringtext){varid=Interlocked.Increment(ref_seq);_messages.Add(newWsMessage{Seq=id,MessageType=type,Text=text});}publicvoidClear(){while(_messages.TryTake(out_)){}Interlocked.Exchange(ref_seq,0);}// 核心切片:只取某一步操作发生后的 WSprivateList<WsMessage>CaptureCore(ActionuiAction,intwaitMs){varstart=Interlocked.Read(ref_seq);uiAction();Thread.Sleep(waitMs);return_messages.Where(m=>m.Seq>start).OrderBy(m=>m.Seq).ToList();}publicList<string>CaptureTexts(ActionuiAction,intwaitMs=1000)=>CaptureCore(uiAction,waitMs).Select(m=>m.Text).ToList();publicList<WsMessage>CaptureRaw(ActionuiAction,intwaitMs=1000)=>CaptureCore(uiAction,waitMs);// 方向过滤(可选:带是否包含空消息参数)publicIEnumerable<string>OnlyReceived(IEnumerable<WsMessage>list,boolcontainEmpty=false){varq=list.Where(m=>m.MessageType=="Received");if(!containEmpty){q=q.Where(m=>!string.IsNullOrWhiteSpace(m.Text)&&m.Text!="{}");}returnq.Select(m=>m.Text);}publicIEnumerable<string>OnlySent(IEnumerable<WsMessage>list,boolcontainEmpty=false){varq=list.Where(m=>m.MessageType=="Sent");if(!containEmpty){q=q.Where(m=>!string.IsNullOrWhiteSpace(m.Text)&&m.Text!="{}");}returnq.Select(m=>m.Text);}publicvoidDispose(){_browser?.CloseAsync().GetAwaiter().GetResult();_playwright?.Dispose();}}5.5 如何在测试类里“正确初始化” Collector(时序写法)
这一段非常重要:端口必须先确定,TestBase 必须用这个端口启动 Chrome,Chrome 启动后 Collector 才能连接。
5.5.1 ClassInitialize:生成端口 + 初始化 Collector,但不连接
staticPlaywrightWsCollector_collector;[ClassInitialize]publicstaticvoidClassInit(TestContextctx){intport=PlaywrightWsCollector.GenerateFreePort();PlaywrightWsCollectorBridge.DebugPort=port;// 让 TestBase 注入该端口_collector=newPlaywrightWsCollector(port);}5.5.2 TestInitialize:先 base,再 EnsureConnected
[TestInitialize]publicoverridevoidTestInitialize(){base.TestInitialize();// 触发 Selenium 启动浏览器(带 remote-debugging-port)_collector.EnsureConnected();// 浏览器已启动,此时才连_collector.Clear();// 每个 case 清空,避免污染}这段顺序颠倒就会出现错误:
ECONNREFUSED:浏览器还没启动就连接- 或端口不一致:Selenium 用 A 端口,Playwright 连 B 端口
5.6 本节小结
到这一节为止,框架已经具备了完整的监听能力:
- Selenium 按原方式执行 UI 操作;
- Playwright 通过 CDP 附着到同一个浏览器;
- 用
CaptureTexts(() => 某一步操作)只截取当前操作的 WS; OnlySent/OnlyReceived可进一步对应 DevTools 的上下箭头;
到这里,我们已经完成了本篇的核心目标:
在不改动既有 Selenium 框架的前提下
通过remote-debugging-port + Playwright CDP 连接
成功监听到了 Selenium 驱动页面产生的WebSocket 消息
并实现了:
- 消息方向区分(Sent / Received)
- 按“单一步操作”切片捕获
- 为后续断言打下基础
这一步,解决了**“拿到 WebSocket”**的问题。
下一篇预告
《使用 Playwright 监听 Selenium 自动化测试中的 WebSocket(二)》
将重点介绍:
- WebSocket 消息中
Action的解析与统计 - SelectionChange / EditRecord 次数断言的实现方式
- PlaywrightWsCollector 在多用例场景下的复用设计
- 常见坑点总结与工程化建议