使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(二)
文章目录
- [使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(二)](#使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(二))
-
- [六、Selenium 与 Playwright 的生命周期协同设计](#六、Selenium 与 Playwright 的生命周期协同设计)
-
- [6.1 一个常见但不明显的问题](#6.1 一个常见但不明显的问题)
- [6.2 一个相对稳妥的调用顺序](#6.2 一个相对稳妥的调用顺序)
- [七、为何需要"按操作切片"的 WebSocket 捕获机制](#七、为何需要“按操作切片”的 WebSocket 捕获机制)
-
- [7.1 问题本质](#7.1 问题本质)
- [7.2 基于序号的切片思路](#7.2 基于序号的切片思路)
- [八、WebSocket 消息方向与 Action 字段的处理策略](#八、WebSocket 消息方向与 Action 字段的处理策略)
-
- [8.1 抽象消息方向](#8.1 抽象消息方向)
- [8.2 关于"空消息"的处理](#8.2 关于“空消息”的处理)
- [九、从 WebSocket 消息中提取并统计 Action](#九、从 WebSocket 消息中提取并统计 Action)
-
- [9.1 Action 提取与计数](#9.1 Action 提取与计数)
- [9.2 在测试中的使用方式](#9.2 在测试中的使用方式)
- 十、实现过程中遇到的典型问题与取舍
-
- [10.1 CDP 连接失败(ECONNREFUSED)](#10.1 CDP 连接失败(ECONNREFUSED))
- [10.2 关于方案边界的说明](#10.2 关于方案边界的说明)
六、Selenium 与 Playwright 的生命周期协同设计
在将 Playwright 引入到现有 Selenium 框架时,真正需要谨慎处理的并不是 API 使用本身,而是两个工具在生命周期上的协同关系。
Playwright 的 ConnectOverCDPAsync 本质上是一个主动连接行为 :
它假设目标浏览器已经启动,并且在指定端口上暴露了 Chrome DevTools Protocol 服务。
如果在浏览器尚未完成启动、或者启动时未正确携带 --remote-debugging-port 参数的情况下尝试连接,连接失败是一个合理且可预期的结果。
6.1 一个常见但不明显的问题
在测试框架中,以下两段逻辑往往分散在不同层级:
- Selenium WebDriver 的创建(通常发生在
TestInitialize或更底层) - Playwright 的 CDP 连接(通常写在测试辅助类或工具类中)
如果不对调用顺序加以控制,很容易出现如下问题:
- Playwright 连接逻辑执行得过早
- 浏览器进程尚未监听调试端口
- 最终表现为
ECONNREFUSED
6.2 一个相对稳妥的调用顺序
在本文的实现中,采用了如下顺序:
-
类级别初始化(ClassInitialize)
- 仅生成调试端口
- 创建 WebSocket 监听器对象(不做连接)
-
用例初始化(TestInitialize)
- 调用
base.TestInitialize(),由 Selenium 启动浏览器 - 浏览器携带
--remote-debugging-port - 再显式调用 Playwright 的
EnsureConnected()
- 调用
我们必须选择在浏览器已经处于稳定运行状态后再附加监听。
七、为何需要"按操作切片"的 WebSocket 捕获机制
在真实的前端系统中,WebSocket 消息往往是持续产生的,并不严格对应某一个 UI 操作。
例如:
- 页面加载完成后的初始化消息
- 心跳包
- 其他用户触发的协同事件
如果在测试中直接对"当前页面收到的所有 WS 消息"做断言,很容易引入噪音,难以判断精确地对指定的操作进行验证。
7.1 问题本质
测试关注的并不是"页面是否收到过某类消息",而是:
"某一个明确的用户操作,是否触发了预期数量和类型的 WebSocket 消息"
因此,测试需要一种方式,将消息流按时间切割。
7.2 基于序号的切片思路
在实现中,并未引入复杂的时间戳或异步同步机制,而是采用了一个简单但可控的方式:
- 为每一条捕获到的 WebSocket 消息分配递增序号
- 在执行 UI 操作前记录当前序号
- 操作完成后,仅保留序号大于该值的消息
示例代码如下:
csharp
private List<WsMessage> CaptureCore(Action uiAction, int waitMs)
{
var startSeq = Interlocked.Read(ref _seq);
uiAction();
Thread.Sleep(waitMs);
return _messages
.Where(m => m.Seq > startSeq)
.OrderBy(m => m.Seq)
.ToList();
}
这种方式并不能保证"绝对只包含该操作的消息",
但在大多数前端交互场景下,可以显著降低噪音并提高断言稳定性。
八、WebSocket 消息方向与 Action 字段的处理策略
在 Chrome DevTools 中,WebSocket 消息通常以"方向"区分:
- 客户端 → 服务端
- 服务端 → 客户端
在 Playwright 中,这一差异体现在两个事件上:
FrameSentFrameReceived
8.1 抽象消息方向
在监听器内部,将方向抽象为字符串标识:
csharp
class WsMessage
{
public long Seq { get; set; }
public string MessageType { get; set; } // Sent / Received
public string Text { get; set; }
}
这种抽象并不依赖具体协议细节,仅用于后续过滤与分析。
8.2 关于"空消息"的处理
在实际观察中,一部分 WebSocket 帧内容可能为:
{}- 空字符串
- 不包含业务字段的控制帧
这些消息在多数业务断言中并不具备价值,因此在工具方法中提供了可选的过滤能力:
csharp
public IEnumerable<string> OnlyReceived(
IEnumerable<WsMessage> list,
bool containEmpty = false)
{
return list
.Where(m => m.MessageType == "Received")
.Where(m => containEmpty ||
(!string.IsNullOrWhiteSpace(m.Text) && m.Text != "{}"))
.Select(m => m.Text);
}
这种设计允许测试在"完整保留原始数据"和"聚焦业务字段"之间进行选择。
九、从 WebSocket 消息中提取并统计 Action
在许多实时系统中,WebSocket 消息的核心语义体现在某个字段上,例如:
json
{
"Action": "EditRecord",
"Data": { ... }
}
测试关注的往往不是消息的完整结构,而是:
- 是否出现了某种 Action
- 出现了几次
- 是否符合预期行为模型
9.1 Action 提取与计数
实现上,采用了较为保守的 JSON 解析方式:
- 非 JSON 或结构异常的消息直接忽略
- 只在存在
Action字段时才参与统计
csharp
private Dictionary<string, int> CountActions(IEnumerable<string> messages)
{
var result = new Dictionary<string, int>();
foreach (var text in messages)
{
if (string.IsNullOrWhiteSpace(text) || text == "{}")
continue;
try
{
var j = JObject.Parse(text);
var action = (string)j["Action"];
if (string.IsNullOrEmpty(action)) continue;
result[action] = result.GetValueOrDefault(action) + 1;
}
catch
{
// 非预期结构,忽略
}
}
return result;
}
9.2 在测试中的使用方式
csharp
var texts = _wsCollector.CaptureTexts(() =>
{
UIAction();
});
var actionCount = CountActions(texts);
Assert.AreEqual(1, actionCount.GetValueOrDefault("SelectionChange"));
Assert.AreEqual(1, actionCount.GetValueOrDefault("EditRecord"));
这种断言方式的优势在于:
- 明确表达"期望行为的次数"
- 对多余或缺失消息都具备可观测性
十、实现过程中遇到的典型问题与取舍
10.1 CDP 连接失败(ECONNREFUSED)
通常由以下原因引起:
- Playwright 连接过早
- 调试端口未正确注入
- 使用
localhost导致 IPv6 解析问题
在实现中,采用了以下应对方式:
- 延迟连接到
TestInitialize之后 - 使用
127.0.0.1显式指定 IPv4
10.2 关于方案边界的说明
该方案的目标并非替代 Selenium,也不是尝试"全面接管浏览器调试能力",而是在既有框架和版本约束下,提供一种可控、可维护的 WebSocket 可观测手段。
在以下情况下,可能需要重新评估方案:
- 对性能指标有强需求
- 需要跨浏览器(非 Chromium)
- 希望统一迁移到 Playwright