使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一)
文章目录
- [使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一)](#使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一))
-
- [一、前言:为什么 Selenium 自动化里"看不到" WebSocket?](#一、前言:为什么 Selenium 自动化里“看不到” WebSocket?)
- [二、整体技术方案:Selenium 驱动,Playwright 监听](#二、整体技术方案:Selenium 驱动,Playwright 监听)
-
- [2.1 方案结构概览](#2.1 方案结构概览)
- [2.2 为什么选择 Playwright?](#2.2 为什么选择 Playwright?)
- 三、环境与前提条件说明
-
- [3.1 技术栈前提](#3.1 技术栈前提)
- [3.2 重要约束条件](#3.2 重要约束条件)
- [四、第一步:让 Selenium 启动一个"可被监听"的浏览器](#四、第一步:让 Selenium 启动一个“可被监听”的浏览器)
-
- [4.1 为什么需要 remote-debugging-port?](#4.1 为什么需要 remote-debugging-port?)
- [4.2 注入 ChromeOptions 的关键时机](#4.2 注入 ChromeOptions 的关键时机)
- [4.3 在 TestBase 中注入 remote-debugging-port](#4.3 在 TestBase 中注入 remote-debugging-port)
- [五、代码实战:实现 PlaywrightWsCollector(专职监听 WebSocket)](#五、代码实战:实现 PlaywrightWsCollector(专职监听 WebSocket))
-
- [5.1 设计一个可复用的消息模型 WsMessage](#5.1 设计一个可复用的消息模型 WsMessage)
- [5.2 端口"桥接"对象(让 TestBase 能拿到端口)](#5.2 端口“桥接”对象(让 TestBase 能拿到端口))
- [5.3 PlaywrightWsCollector:核心类骨架](#5.3 PlaywrightWsCollector:核心类骨架)
-
- [5.3.1 生成空闲端口(多机/多进程安全)](#5.3.1 生成空闲端口(多机/多进程安全))
- [5.4 完整实现:PlaywrightWsCollector.cs](#5.4 完整实现:PlaywrightWsCollector.cs)
- [5.5 如何在测试类里"正确初始化" Collector(时序写法)](#5.5 如何在测试类里“正确初始化” Collector(时序写法))
-
- [5.5.1 ClassInitialize:生成端口 + 初始化 Collector,但不连接](#5.5.1 ClassInitialize:生成端口 + 初始化 Collector,但不连接)
- [5.5.2 TestInitialize:先 base,再 EnsureConnected](#5.5.2 TestInitialize:先 base,再 EnsureConnected)
- [5.6 本节小结](#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 WebDriver 3.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时才真正启动浏览器
幸运的是,大多数成熟框架都会提供一个生命周期钩子,例如:
csharp
protected virtual void AfterInitializeTestDriver()
这个方法的特点是:
- Browser 已创建
- WebDriver 尚未创建
- 正是注入 ChromeOptions 的最佳时机
4.3 在 TestBase 中注入 remote-debugging-port
示例代码如下:
csharp
protected override void AfterInitializeTestDriver()
{
base.AfterInitializeTestDriver();
if (PlaywrightWsCollectorBridge.DebugPort == 0)
return;
var browser = this.ActiveBrowser;
if (browser.BrowserType == BrowserType.Chrome)
{
var options = new ChromeOptions();
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)
- 以及消息发生顺序(用于"操作切片")
csharp
public class WsMessage
{
public long Seq { get; set; } // 递增序号:用于切片
public string MessageType { get; set; } // "Sent" / "Received"
public string Text { get; set; } // WebSocket 帧内容(一般是 JSON)
}
5.2 端口"桥接"对象(让 TestBase 能拿到端口)
在第 4 节里已经在 TestBase.AfterInitializeTestDriver() 注入了:
csharp
--remote-debugging-port=xxx
但 TestBase 自己并不知道端口是什么,因此我们用一个小桥接类存放端口值即可:
csharp
public static class PlaywrightWsCollectorBridge
{
public static int DebugPort { get; set; } = 0;
}
约定:
DebugPort == 0:表示当前用例不需要 WS 监听;- 非 0:TestBase 注入该端口,Playwright 用同一个端口连接。
5.3 PlaywrightWsCollector:核心类骨架
这个类负责:
- 生成一个可用端口(供 Selenium 启动 Chrome)
- 在浏览器启动后,通过 CDP 连接
- hook page 的 WebSocket 事件
- 收集消息、切片、分类、清理
关键点:不要在构造函数里 Connect ,否则浏览器还没启动就连,会直接 ECONNREFUSED。
5.3.1 生成空闲端口(多机/多进程安全)
csharp
using System.Net;
using System.Net.Sockets;
public static int GenerateFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
5.4 完整实现:PlaywrightWsCollector.cs
说明:这里使用
127.0.0.1,避免某些机器localhost解析到 IPv6::1导致连接异常。
csharp
using Microsoft.Playwright;
using System.Collections.Concurrent;
using System.Threading;
using System.Linq;
public class PlaywrightWsCollector : IDisposable
{
private IPlaywright _playwright;
private IBrowser _browser;
private readonly ConcurrentBag<WsMessage> _messages = new ConcurrentBag<WsMessage>();
private long _seq = 0;
private bool _connected = false;
private bool _pageHooked = false;
public int DebugPort { get; }
public PlaywrightWsCollector(int debugPort)
{
DebugPort = debugPort; // 仅保存端口,不做连接
}
/// <summary>
/// 在 Selenium 浏览器启动(带 remote-debugging-port)后调用。
/// </summary>
public void EnsureConnected()
{
if (_connected) return;
_playwright = Playwright.CreateAsync().GetAwaiter().GetResult();
var url = $"http://127.0.0.1:{DebugPort}";
_browser = _playwright.Chromium.ConnectOverCDPAsync(url).GetAwaiter().GetResult();
HookExistingContexts(_browser);
_connected = true;
}
private void HookExistingContexts(IBrowser browser)
{
// .NET 里没有 IBrowser.ContextCreated,所以只能:
// 1) 先 hook 现有 contexts
foreach (var ctx in browser.Contexts)
{
HookContext(ctx);
}
}
private void HookContext(IBrowserContext ctx)
{
// hook 已有 page
foreach (var page in ctx.Pages)
{
HookPage(page);
}
// hook 新打开的 page(例如 Selenium 新开 tab/window)
ctx.Page += (_, page) => HookPage(page);
}
private void HookPage(IPage page)
{
// 避免重复 hook:同一个测试通常只关心当前页面的 WS
if (_pageHooked) return;
page.WebSocket += (sender, ws) =>
{
ws.FrameSent += (s2, frame) =>
{
AddWs("Sent", frame.Text);
};
ws.FrameReceived += (s3, frame) =>
{
AddWs("Received", frame.Text);
};
};
_pageHooked = true;
}
private void AddWs(string type, string text)
{
var id = Interlocked.Increment(ref _seq);
_messages.Add(new WsMessage
{
Seq = id,
MessageType = type,
Text = text
});
}
public void Clear()
{
while (_messages.TryTake(out _)) { }
Interlocked.Exchange(ref _seq, 0);
}
// 核心切片:只取某一步操作发生后的 WS
private List<WsMessage> CaptureCore(Action uiAction, int waitMs)
{
var start = Interlocked.Read(ref _seq);
uiAction();
Thread.Sleep(waitMs);
return _messages
.Where(m => m.Seq > start)
.OrderBy(m => m.Seq)
.ToList();
}
public List<string> CaptureTexts(Action uiAction, int waitMs = 1000)
=> CaptureCore(uiAction, waitMs).Select(m => m.Text).ToList();
public List<WsMessage> CaptureRaw(Action uiAction, int waitMs = 1000)
=> CaptureCore(uiAction, waitMs);
// 方向过滤(可选:带是否包含空消息参数)
public IEnumerable<string> OnlyReceived(IEnumerable<WsMessage> list, bool containEmpty = false)
{
var q = list.Where(m => m.MessageType == "Received");
if (!containEmpty)
{
q = q.Where(m => !string.IsNullOrWhiteSpace(m.Text) && m.Text != "{}");
}
return q.Select(m => m.Text);
}
public IEnumerable<string> OnlySent(IEnumerable<WsMessage> list, bool containEmpty = false)
{
var q = list.Where(m => m.MessageType == "Sent");
if (!containEmpty)
{
q = q.Where(m => !string.IsNullOrWhiteSpace(m.Text) && m.Text != "{}");
}
return q.Select(m => m.Text);
}
public void Dispose()
{
_browser?.CloseAsync().GetAwaiter().GetResult();
_playwright?.Dispose();
}
}
5.5 如何在测试类里"正确初始化" Collector(时序写法)
这一段非常重要:端口必须先确定,TestBase 必须用这个端口启动 Chrome,Chrome 启动后 Collector 才能连接。
5.5.1 ClassInitialize:生成端口 + 初始化 Collector,但不连接
csharp
static PlaywrightWsCollector _collector;
[ClassInitialize]
public static void ClassInit(TestContext ctx)
{
int port = PlaywrightWsCollector.GenerateFreePort();
PlaywrightWsCollectorBridge.DebugPort = port; // 让 TestBase 注入该端口
_collector = new PlaywrightWsCollector(port);
}
5.5.2 TestInitialize:先 base,再 EnsureConnected
csharp
[TestInitialize]
public override void TestInitialize()
{
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 在多用例场景下的复用设计
- 常见坑点总结与工程化建议