基于Playwright的WebRTC自动化测试:模拟设备、网络与多用户场景实践

发布时间:2026/7/4 12:37:21
基于Playwright的WebRTC自动化测试:模拟设备、网络与多用户场景实践 1. 项目概述当WebRTC遇上Playwright实时通信测试的破局之路如果你正在开发或维护一个依赖WebRTCWeb实时通信技术的应用比如视频会议、在线教育、远程协作或者实时游戏那你一定对测试这件事感到头疼。这玩意儿不像普通的表单提交或者页面跳转点一下等结果就行。WebRTC测试是个多维度的复杂工程它涉及音视频流的采集、编码、网络传输、解码、渲染还要处理NAT穿透、信令交互、网络抖动和丢包。传统的基于Selenium的UI自动化测试在模拟摄像头、麦克风权限以及精确控制网络带宽和延迟方面往往力不从心更别提深入获取媒体流的内部状态比如码率、帧率、丢包率了。这就是为什么我们需要把目光投向Playwright。它不仅仅是一个“更好的Selenium”而是一个为现代Web应用尤其是那些重度依赖媒体和网络API的应用量身定制的自动化与测试框架。我最近在一个大型视频会议项目的质量保障中深度使用了Playwright with Python来构建WebRTC自动化测试体系实实在在地解决了从基础功能验证到复杂场景模拟的一系列难题。这篇文章我就来拆解其中的三大核心技术分享如何用Playwright Python搭建一套可靠、高效且可维护的WebRTC自动化测试方案无论是测试工程师还是开发工程师都能从中找到可以直接“抄作业”的实践。2. 核心技术一模拟真实环境的媒体设备与权限控制WebRTC测试的第一个拦路虎就是媒体设备。你不可能要求每台测试机都配备高清摄像头和降噪麦克风更不可能在CI/CD流水线里插满USB设备。Playwright在这方面提供了两种优雅的解决方案使用虚拟Fake设备或者注入自定义媒体流。选择哪种取决于你的测试目标。2.1 虚拟设备快速启动功能测试对于绝大多数功能测试例如验证“开始通话”按钮点击后本地视频画面能否正常显示使用虚拟设备是最快捷、最稳定的方式。它的原理是让浏览器使用内置的虚拟视频和音频源而不是调用真实的硬件。import asyncio from playwright.async_api import async_playwright async def test_basic_webrtc_connection(): async with async_playwright() as p: # 启动浏览器关键参数在这里 browser await p.chromium.launch( headlessFalse, # 调试时可设为False观察界面 args[ --use-fake-ui-for-media-stream, # 关键跳过真实的权限弹窗UI --use-fake-device-for-media-stream, # 关键使用虚拟的摄像头和麦克风 --use-file-for-fake-video-capture/path/to/test.y4m, # 可选指定虚拟视频文件 --use-file-for-fake-audio-capture/path/to/test.wav # 可选指定虚拟音频文件 ] ) # 创建上下文时直接授予权限避免弹窗干扰 context await browser.new_context( permissions[camera, microphone] ) page await context.new_page() await page.goto(https://your-webrtc-app.com) # 此时页面调用 navigator.mediaDevices.getUserMedia 将直接获得虚拟流无需人工交互 await page.click(button#start-video) # 可以断言视频元素是否处于活跃状态 video_element page.locator(video#local-video) await expect(video_element).to_have_js_property(readyState, 4) # HAVE_ENOUGH_DATA await browser.close()实操心得与避坑指南headless模式的选择在调试阶段建议使用headlessFalse以便观察浏览器行为。但在CI/CD环境中务必使用headlessTrue或headlessnew以提高性能并避免无头环境下的潜在问题。Playwright的“new”头less模式兼容性更好。虚拟文件源--use-file-for-fake-video-capture参数非常有用。你可以准备一个Y4M格式的测试视频循环播放。这能确保每次测试的视频内容一致便于做图像质量分析的基准对比。没有这个参数虚拟设备生成的是动态彩色条纹每次可能不同。权限上下文隔离通过browser.new_context(permissions...)授予权限而不是在page层面处理。这样做的好处是同一个浏览器实例下的不同上下文可以理解为不同的隐身窗口可以拥有独立的权限集非常适合模拟多个用户同时测试的场景。2.2 自定义媒体流注入精准控制测试数据当你的测试需要验证特定的媒体处理逻辑时比如测试美颜滤镜效果、或验证客户端能否正确处理某种特定编码格式的视频虚拟设备可能就不够用了。这时我们需要在页面加载前向浏览器上下文中注入脚本彻底重写getUserMediaAPI。async def test_with_custom_media_stream(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) context await browser.new_context(permissions[camera, microphone]) # 在页面加载任何内容之前注入自定义脚本 await context.add_init_script( // 创建一个返回自定义MediaStream的函数 window.createCustomMediaStream async (constraints) { const canvas document.createElement(canvas); canvas.width 640; canvas.height 480; const ctx canvas.getContext(2d); // 模拟一个动态变化的视频帧例如移动的方块 function drawFrame(timestamp) { ctx.fillStyle #2d2d2d; ctx.fillRect(0, 0, canvas.width, canvas.height); const x (timestamp / 50) % canvas.width; ctx.fillStyle #4CAF50; ctx.fillRect(x, 100, 100, 100); ctx.fillStyle #fff; ctx.font 20px Arial; ctx.fillText(Test Frame: ${Math.floor(timestamp)}, 50, 50); } // 创建CanvasCaptureMediaStreamTrack const stream canvas.captureStream(30); // 30 fps const track stream.getVideoTracks()[0]; // 模拟一个简单的音频轨道静音 const audioContext new (window.AudioContext || window.webkitAudioContext)(); const oscillator audioContext.createOscillator(); const dst oscillator.connect(audioContext.createMediaStreamDestination()); oscillator.start(); const audioTrack dst.stream.getAudioTracks()[0]; // 根据constraints决定返回哪些轨道 const tracks []; if (constraints.video ! false constraints.video ! undefined) { tracks.push(track); } if (constraints.audio ! false constraints.audio ! undefined) { tracks.push(audioTrack); } return new MediaStream(tracks); }; // 重写getUserMedia navigator.mediaDevices.getUserMedia async (constraints) { console.log([Playwright Mock] getUserMedia called with:, constraints); return await window.createCustomMediaStream(constraints); }; ) page await context.new_page() await page.goto(https://your-webrtc-app.com) # 现在页面中的任何getUserMedia调用都会得到我们自定义的流 # 我们可以进一步在注入的脚本里暴露钩子函数让测试代码能动态控制流的内容 await page.evaluate(() { window.injectedStreamController { changeVideoColor: function(color) { // 实现改变Canvas绘制颜色的逻辑 console.log(Changing video color to, color); } }; }) # 在测试过程中可以动态改变媒体流 await page.evaluate(() window.injectedStreamController.changeVideoColor(#FF5733)) await browser.close()注意事项执行时机至关重要add_init_script必须在page.goto之前调用确保脚本在页面自身的JavaScript执行前就已生效。模拟的完备性自定义流需要模拟MediaStreamTrack的常用方法和事件如stop(),onended否则页面代码调用这些方法时可能会出错。上面的示例是一个简化版生产级测试需要更完整的模拟。与真实API的差异完全重写API可能会掩盖一些真实的浏览器兼容性问题。因此这套方案更适合用于验证业务逻辑最终的兼容性测试仍需结合真实或虚拟设备进行。3. 核心技术二精细化网络模拟与性能指标采集WebRTC的核心挑战在网络。用户可能处在4G、5G、Wi-Fi或糟糕的公共网络下。Playwright提供了强大的网络模拟能力可以精确控制带宽、延迟、丢包率让我们能在可控环境下复现各种网络问题。3.1 模拟复杂网络条件Playwright的BrowserContext对象提供了set_network_conditions方法这是进行网络模拟的主力。import asyncio from playwright.async_api import async_playwright class WebRTCNetworkTest: def __init__(self): self.metrics [] async def run_network_scenario(self, scenario_name, download_kbps, upload_kbps, latency_ms, packet_loss_rate0): 运行特定网络场景下的测试 async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) # 注意网络条件是在context级别设置的 context await browser.new_context( permissions[camera, microphone], # 通过viewport模拟不同设备网络测试常结合进行 viewport{width: 1920, height: 1080} ) page await context.new_page() # 1. 先以良好网络条件加载页面建立基础连接 await page.goto(https://your-webrtc-app.com, wait_untilnetworkidle) await page.click(button#start-call) await page.wait_for_selector(.peer-connected, timeout10000) # 2. 应用目标网络条件 print(fApplying network condition: {scenario_name}) await context.set_network_conditions( offlineFalse, download_throughputdownload_kbps * 1024, # 转换为bps upload_throughputupload_kbps * 1024, latencylatency_ms ) # 注意Playwright原生API不支持直接设置丢包率。需要通过路由route拦截并随机丢弃请求来模拟。 # 这里先记录后续会讲到高级模拟方法。 # 3. 在恶劣网络下稳定运行一段时间收集数据 await asyncio.sleep(30) # 模拟持续30秒的恶劣网络 # 4. 采集关键性能指标下一节详述 stats await self._collect_webrtc_stats(page) stats[scenario] scenario_name self.metrics.append(stats) # 5. 恢复良好网络测试恢复能力 await context.set_network_conditions( offlineFalse, download_throughput50 * 1024 * 1024, # 50 Mbps upload_throughput10 * 1024 * 1024, # 10 Mbps latency5 ) await asyncio.sleep(10) recovery_stats await self._collect_webrtc_stats(page) print(fRecovery stats for {scenario_name}: {recovery_stats}) await browser.close() async def run_all_scenarios(self): 定义并运行一系列典型网络场景 scenarios [ (Excellent (Fiber), 50000, 10000, 5, 0), # 光纤50Mbps下行10Mbps上行5ms延迟 (Good (4G), 10000, 2000, 50, 0.1), # 4G10Mbps下行2Mbps上行50ms延迟0.1%丢包 (Average (3G), 2000, 500, 150, 0.5), # 3G (Poor (2G), 500, 100, 300, 1), # 2G (Very Poor, 100, 50, 500, 2), # 极差网络 ] for scenario in scenarios: name, dl, ul, lat, loss scenario await self.run_network_scenario(name, dl, ul, lat, loss) self._generate_report()3.2 高级网络模拟丢包、抖动与节流Playwright原生API对丢包和网络抖动的支持有限。要实现更真实的模拟我们需要结合使用路由拦截Route和自定义逻辑。async def simulate_packet_loss_and_jitter(page, loss_rate0.01, jitter_ms50): 通过拦截WebSocket和HTTP请求模拟丢包和抖动。 注意这主要影响信令和数据通道对SRTP/RTP媒体流的模拟需要更底层的方法。 # 拦截所有请求 await page.route(**/*, lambda route: handle_route(route, loss_rate, jitter_ms)) async def handle_route(route, loss_rate, jitter_ms): import random, time request route.request # 模拟丢包随机丢弃一定比例的请求 if random.random() loss_rate: print(f[Network Sim] Packet lost for: {request.url}) # 可以选择直接abort或者返回一个错误响应 await route.abort() return # 模拟网络抖动随机增加延迟 delay random.randint(0, jitter_ms) / 1000.0 # 转换为秒 if delay 0: await asyncio.sleep(delay) # 继续处理请求 await route.continue_()重要提示上述方法模拟的是HTTP/WebSocket层的丢包和延迟这对于测试信令服务器的交互和DataChannel非常有效。但要模拟媒体流RTP/RTCP的丢包和抖动Playwright的浏览器上下文API无法直接做到。这通常需要更底层的工具例如使用系统级网络模拟工具在运行Playwright的宿主机或容器内使用tc(Linux Traffic Control) 或Network Link Conditioner(macOS) 来塑造整个网络接口的流量。这会影响所有进出浏览器的数据包包括媒体流。使用Docker网络在Docker容器中运行浏览器并利用容器的网络命名空间配合tc进行模拟。专门的测试服务/设备对于实验室环境可以使用如Apposite Technologies的硬件模拟器或类似的软件方案。在大多数自动化测试场景中如果主要验证应用层逻辑如网络切换时的UI提示、重连机制使用Playwright的set_network_conditions结合HTTP层拦截已经足够。如果需要端到端的媒体质量评估建议将系统级网络模拟作为测试环境的一部分进行配置。3.3 采集WebRTC内部统计信息这是评估通话质量的核心。我们需要从浏览器的RTCPeerConnection中获取RTCStatsReport。async def _collect_webrtc_stats(self, page): 从页面中收集WebRTC统计信息 stats await page.evaluate(async () { // 假设页面上有一个全局可访问的peerConnection对象 // 实际中可能需要通过页面暴露的API或遍历window对象来找到它 if (!window.myPeerConnection) { console.warn(PeerConnection not found in window object.); return null; } const report await window.myPeerConnection.getStats(); const result { timestamp: Date.now(), inbound: {}, outbound: {}, candidatePair: {} }; report.forEach(stat { // 收集入站视频流统计 if (stat.type inbound-rtp stat.kind video) { result.inbound.video { bitrate: stat.bytesReceived * 8 / (stat.timestamp - (result.inbound.video?.lastTimestamp || stat.timestamp)) || 0, packetsLost: stat.packetsLost, jitter: stat.jitter, framerate: stat.framesPerSecond, resolution: ${stat.frameWidth}x${stat.frameHeight} }; result.inbound.video.lastTimestamp stat.timestamp; } // 收集出站视频流统计 if (stat.type outbound-rtp stat.kind video) { result.outbound.video { bitrate: stat.bytesSent * 8 / (stat.timestamp - (result.outbound.video?.lastTimestamp || stat.timestamp)) || 0, packetsSent: stat.packetsSent, }; result.outbound.video.lastTimestamp stat.timestamp; } // 收集候选对信息关键的网络层指标 if (stat.type candidate-pair stat.nominated) { result.candidatePair { currentRoundTripTime: stat.currentRoundTripTime, availableOutgoingBitrate: stat.availableOutgoingBitrate, requestsReceived: stat.requestsReceived, responsesReceived: stat.responsesReceived }; } }); return result; }) if stats: # 计算平均码率等衍生指标 self._calculate_derived_metrics(stats) return stats实操心得访问RTCPeerConnection对象这是最大的挑战。如果被测应用没有将peerConnection暴露在全局作用域window你需要与开发团队协商在测试构建版本中注入一个钩子函数或者通过遍历window对象和已知的变量名来尝试查找。定时采集你需要在一个循环中定期调用这个统计收集函数以绘制出码率、丢包率随时间变化的曲线这对于分析网络条件变化的影响至关重要。指标解读currentRoundTripTime(RTT) 和availableOutgoingBitrate是判断网络健康度的黄金指标。RTT突然飙升通常意味着拥塞可用带宽下降则可能导致视频质量自动降级。4. 核心技术三多浏览器实例与复杂场景编排真实的WebRTC应用往往是多用户的。测试单人通话只是第一步我们需要模拟会议室、多人游戏等场景。Playwright可以轻松创建和管理多个独立的浏览器上下文或实例来模拟不同的用户。4.1 模拟多用户加入会议室import asyncio from playwright.async_api import async_playwright class MultiUserConferenceTest: async def setup_users(self, user_count3): 创建多个独立的浏览器上下文来模拟多个用户 self.users [] async with async_playwright() as p: for i in range(user_count): # 每个用户拥有独立的浏览器上下文完全隔离cookies, localStorage, 权限等 browser await p.chromium.launch(headlessTrue) context await browser.new_context( permissions[camera, microphone], viewport{width: 800, height: 600}, # 可以为每个用户设置不同的地理位置如果需要 geolocation{latitude: 40.7 i*0.1, longitude: -74.0 i*0.1}, localeen-US ) # 为每个上下文设置不同的网络条件模拟用户在不同网络下 if i 0: await context.set_network_conditions(download_throughput10*1024*1024, upload_throughput2*1024*1024, latency20) # 用户0好网络 elif i 1: await context.set_network_conditions(download_throughput2*1024*1024, upload_throughput512*1024, latency100) # 用户1一般网络 page await context.new_page() await page.goto(https://your-meeting-app.com) self.users.append({ id: fuser_{i}, browser: browser, context: context, page: page }) async def test_conference_join_and_media(self): 测试所有用户加入会议室并检查媒体流 join_tasks [] for user in self.users: # 并行执行加入操作模拟真实场景 task asyncio.create_task(self._user_join_meeting(user, test-room-123)) join_tasks.append(task) # 等待所有用户加入完成 await asyncio.gather(*join_tasks) print(All users joined the meeting.) # 验证每个用户的本地视频是否就绪 for user in self.users: local_video user[page].locator(video#local-video) await expect(local_video).to_be_visible(timeout15000) await expect(local_video).to_have_js_property(readyState, 4) # 验证每个用户都能看到其他用户的视频远程视频 # 假设页面为每个远程用户动态生成一个 video 元素其id包含对方用户ID for i, user in enumerate(self.users): for j, other_user in enumerate(self.users): if i ! j: remote_video_selector fvideo[data-peer-id{other_user[id]}] remote_video user[page].locator(remote_video_selector) # 等待远程视频元素出现并开始播放 await expect(remote_video).to_be_attached(timeout20000) # 可以进一步检查视频是否正在播放 is_playing await remote_video.evaluate(v !v.paused v.readyState 2) assert is_playing, fUser {user[id]} cannot see playing video from {other_user[id]} print(All media streams verified.) async def _user_join_meeting(self, user, room_id): 单个用户加入会议室的流程 page user[page] await page.fill(input#room-id, room_id) await page.fill(input#username, user[id]) await page.click(button#join-button) # 等待加入成功的UI反馈 await page.wait_for_selector(.in-meeting, timeout10000)4.2 模拟用户交互与异常场景自动化测试不仅要覆盖“阳光路径”更要覆盖“风雨路径”。async def test_screen_sharing(self): 测试用户发起屏幕共享其他人能否看到 sharer self.users[0] await sharer[page].click(button#share-screen) # 处理屏幕选择弹窗Playwright可以模拟选择整个屏幕、应用窗口或标签页 # 注意headless模式下屏幕共享可能受限需要特定标志或使用非headless模式。 # 这里假设应用使用getDisplayMedia并且我们通过之前add_init_script的方式模拟了它。 # 验证共享者界面显示“正在共享”状态 await expect(sharer[page].locator(.screen-share-active)).to_be_visible(timeout5000) # 验证其他用户界面出现屏幕共享视频 for viewer in self.users[1:]: screen_share_video viewer[page].locator(video.screen-share) await expect(screen_share_video).to_be_visible(timeout10000) is_playing await screen_share_video.evaluate(v !v.paused) assert is_playing, fViewer {viewer[id]} cannot see playing screen share. async def test_network_disconnect_recovery(self): 模拟用户网络中断并重连 victim self.users[1] # 1. 记录当前状态 initial_peers await victim[page].locator(.remote-peer).count() # 2. 模拟网络中断关闭浏览器上下文级别的网络 await victim[context].set_network_conditions( offlineTrue # 关键设置为离线 ) print(fNetwork disconnected for {victim[id]}) await asyncio.sleep(10) # 保持离线10秒 # 3. 验证应用是否检测到断线并显示相应UI如“连接中断” await expect(victim[page].locator(.connection-lost)).to_be_visible(timeout8000) # 4. 恢复网络 await victim[context].set_network_conditions(offlineFalse) print(fNetwork restored for {victim[id]}) # 5. 验证自动重连或提示用户手动重连 # 方案A等待自动重连成功 await victim[page].wait_for_selector(.connection-reestablished, timeout30000) # 方案B可能需要用户点击重连按钮 # await victim[page].click(button#reconnect) # await victim[page].wait_for_selector(.in-meeting, timeout15000) # 6. 验证媒体流恢复 await expect(victim[page].locator(video#local-video)).to_have_js_property(readyState, 4, timeout15000) final_peers await victim[page].locator(.remote-peer).count() assert final_peers initial_peers, Peer count did not recover after reconnect. async def cleanup(self): 清理所有浏览器实例 for user in self.users: await user[context].close() await user[browser].close()编排与并发技巧使用asyncio.gather对于可以并行执行的操作如多个用户同时加入房间使用asyncio.gather能显著缩短测试总时间。资源管理每个浏览器实例和上下文都会消耗内存和CPU。在测试完成后务必调用close()方法妥善关闭避免资源泄漏。可以考虑使用async with语句块来管理生命周期。测试数据隔离每个浏览器上下文是隔离的这意味着它们的cookie、localStorage、sessionStorage互不影响完美模拟了多个独立用户设备。5. 构建健壮的测试框架与持续集成将上述技术点组合起来形成一个可维护、可扩展的自动化测试框架并集成到CI/CD流水线中才能发挥最大价值。5.1 测试框架设计我推荐使用pytest作为测试运行器它功能强大插件生态丰富如pytest-asyncio用于异步支持pytest-html用于生成报告。# conftest.py - 定义pytest fixtures import pytest import asyncio from playwright.async_api import async_playwright pytest.fixture(scopesession) def event_loop(): 为异步测试创建事件循环 loop asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() pytest.fixture(scopefunction) # 每个测试函数一个独立的浏览器上下文 async def browser_context(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) context await browser.new_context( permissions[camera, microphone], viewport{width: 1280, height: 720}, record_video_dirvideos/ # 可选录制测试视频 ) yield context await context.close() await browser.close() pytest.fixture async def page(browser_context): page await browser_context.new_page() yield page await page.close() # test_webrtc_basic.py import pytest class TestWebRTCBasic: pytest.mark.asyncio async def test_single_peer_connection(self, page): 测试点对点连接建立 await page.goto(https://app.example.com/p2p) await page.click(#startButton) # ... 具体的断言逻辑 assert await page.locator(.status).text_content() connected pytest.mark.asyncio pytest.mark.parametrize(network_condition, [good, average, poor]) async def test_connection_under_network_stress(self, page, network_condition): 参数化测试在不同网络压力下测试连接 # 根据参数设置网络条件 condition_map {good: (5000, 1000, 20), average: (1000, 500, 100), poor: (300, 100, 300)} dl, ul, lat condition_map[network_condition] await page.context.set_network_conditions(download_throughputdl*1024, upload_throughputul*1024, latencylat) await page.goto(https://app.example.com/p2p) await page.click(#startButton) # 断言在限定时间内应能连接成功或优雅降级 try: await page.wait_for_selector(.connected, timeout30000) assert True except: # 在极差网络下连接可能失败但应用应显示相应提示 await page.wait_for_selector(.connection-failed, timeout5000) assert await page.locator(.error-message).is_visible()5.2 集成到CI/CD流水线在GitHub Actions、GitLab CI或Jenkins中你需要确保环境具备必要的依赖。# .github/workflows/webrtc-e2e.yml 示例 name: WebRTC E2E Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install system dependencies (for Playwright browsers) run: | sudo apt-get update sudo apt-get install -y libgbm-dev libnss3 libatk-bridge2.0-0 libdrm-dev libxkbcommon-x11-0 libasound2 - name: Install Python dependencies run: | pip install -r requirements.txt pip install playwright pytest pytest-asyncio pytest-html - name: Install Playwright browsers run: python -m playwright install chromium --with-deps - name: Run WebRTC E2E tests run: | pytest tests/webrtc/ -v --htmlreport.html --self-contained-html env: # 传递测试所需的配置如被测应用URL、测试房间密码等 TEST_APP_URL: ${{ secrets.TEST_APP_URL }} - name: Upload test artifacts if: always() # 即使测试失败也上传 uses: actions/upload-artifactv3 with: name: playwright-report path: | report.html videos/ # 如果录制了视频 screenshots/ # 如果测试失败时截图5.3 常见问题排查与技巧实录在实际操作中我踩过不少坑这里分享几个高频问题的解决方案权限弹窗处理失败现象测试卡住等待用户允许摄像头/麦克风。解决确保在browser.new_context()时通过permissions参数预先授予权限。如果应用在页面加载后动态请求权限可以使用page.context.grant_permissions([camera, microphone])。最根本的方法是使用--use-fake-ui-for-media-stream启动参数彻底绕过弹窗。Headless模式下视频黑屏或无法播放现象在CI的无头环境中视频元素的readyState始终为0。解决首先确认使用了虚拟设备参数--use-fake-device-for-media-stream。其次某些WebRTC库或浏览器在headless模式下可能需要额外的标志来启用GPU和媒体功能。尝试添加这些Chrome启动参数--enable-gpu-rasterization--enable-featuresVizDisplayCompositor。如果问题依旧考虑在调试期使用headlessnew或headlessFalse进行对比。无法获取页面内的RTCPeerConnection对象现象page.evaluate()中找不到window.myPeerConnection。解决这是测试架构问题。有三种策略合作式与开发团队约定在测试环境构建中将关键的连接对象挂载到window的一个特定属性下如window.__testPeerConnection。侵入式在页面加载前通过add_init_script注入代码劫持RTCPeerConnection构造函数将创建的实例收集到一个全局数组中供测试脚本访问。非侵入式通过遍历window对象寻找可能是RTCPeerConnection的实例判断其是否有getStats等方法。这种方法最脆弱但有时是唯一选择。测试不稳定时好时坏现象断言偶尔失败尤其是涉及网络状态或元素可见性的断言。解决增加超时时间WebRTC建立连接、交换候选地址需要时间特别是在模拟慢网络时。将wait_for_selector、wait_for_function的超时时间设置得足够长例如15-30秒。使用更健壮的定位器避免使用基于文本或容易变化的CSS选择器。优先使用>