【获取WebSocket】使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(一)

使用 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 重要约束条件

这是本文方案成立的关键前提:

  1. 不能修改 Selenium Browser 核心实现

    • CreateWebDriver() 是私有方法
    • Browser 类可能来自 DLL
  2. 但可以:

    • 修改 TestBase
    • 在测试类中增加辅助逻辑
  3. 测试用例可能在:

    • 多台机器
    • 多进程
    • 并发执行

因此,本方案必须满足:

  • 对现有框架侵入极小
  • 不引入端口冲突
  • 不影响非 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);
    }
}

这里做了三件关键的事:

  1. 仅在需要 WebSocket 的测试中生效(通过 DebugPort 判断)
  2. 使用 Chrome 原生支持的 --remote-debugging-port
  3. 不修改 Browser 内部实现,只调用公开扩展点

到这里,Selenium 启动的 Chrome 已经具备了"被 Playwright 监听"的能力。


五、代码实战:实现 PlaywrightWsCollector(专职监听 WebSocket)

这一节的目标很明确:把"DevTools 里 WS → Messages 面板看到的东西",变成测试代码里可读取、可切片、可断言的数据结构。

同时要满足几个工程要求:

  • Playwright 不接管 UI,只监听;
  • 连接必须通过 CDP 端口,而端口是 Selenium 启动 Chrome 时注入的;
  • 避免常见坑:.NET 没有 IBrowser.ContextCreatedECONNREFUSED 等;
  • 能做到"只取某一步操作产生的 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:核心类骨架

这个类负责:

  1. 生成一个可用端口(供 Selenium 启动 Chrome)
  2. 在浏览器启动后,通过 CDP 连接
  3. hook page 的 WebSocket 事件
  4. 收集消息、切片、分类、清理

关键点:不要在构造函数里 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 在多用例场景下的复用设计
  • 常见坑点总结与工程化建议
相关推荐
xiaohai@Linux8 小时前
基于 TCP 的IOT物联网云端服务端和设备客户端通信架构设计与实现
嵌入式硬件·物联网·网络协议·tcp/ip
尼罗河女娲8 小时前
【测试开发】Selenium + Chrome 自动化中常用 ChromeOptions 参数说明(实践总结)
chrome·selenium·自动化
while(1){yan}8 小时前
HTTP的加密过程
java·开发语言·网络·网络协议·http·青少年编程
重生之我是Java开发战士9 小时前
【计算机网络】数据链路层:从帧传输到高速以太网的完整梳理
网络·网络协议·计算机网络
爱打代码的小林9 小时前
python(爬虫selenium)
爬虫·python·selenium
尼罗河女娲9 小时前
【获取WebSocket】使用 Playwright 监听 Selenium 自动化测试中的 WebSocket 消息(二)
websocket·网络协议·selenium
mikejahn9 小时前
使用selenium从CNAS网站解析实验室信息
python·selenium·测试工具
了一梨9 小时前
网络编程:UDP Socket
linux·网络协议·udp
云和数据.ChenGuang9 小时前
F5 Big-IP by SNMP.硬件负载均衡
网络协议·tcp/ip·负载均衡·数据库运维工程师·运维教程