发布时间:2026/9/7 23:31:46
使用 Playwright 自动化 Microsoft Edge WebView2:通过 CDP 连接与多语言测试实战 使用 Playwright 自动化 Microsoft Edge WebView2通过 CDP 连接与多语言测试实战【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright本文以当前 Playwright 仓库中的官方文档 webview2.md 为主体系统讲解如何让 Playwright 驱动内嵌在 Windows 桌面应用中的 Microsoft Edge WebView2 控件从在宿主应用中开启 Chrome DevTools ProtocolCDP远程调试端口到用connectOverCDP建立连接再到用 JavaScript/TypeScript、Java、Python、C#MSTest/NUnit/xUnit搭建相互隔离的并行测试脚手架。读完本文你可以把 WebView2 当成一个可被 Playwright 操控的浏览器目标对桌面应用内嵌网页进行端到端测试并理解其底层实现原理。WebView2 与 Playwright 的接入方式Microsoft Edge WebView2 是一个 Windows 窗体WinForms / WPF控件底层使用 Microsoft Edge 渲染网页内容属于 Edge 浏览器的一部分可用于 Windows 10 与 Windows 11 桌面应用。Playwright 可以自动化 WebView2 应用、测试其中的 Web 内容——接入方式是让 WebView2 暴露 CDP 调试端口再由 Playwright 通过connectOverCDP建立连接。从实现上看connectOverCDP并非 WebView2 独有机制它被设计为「连接任何已开启 CDP 调试端口的 Chromium 内核目标」。客户端 API 位于 BrowserType.connectOverCDP自 v1.9 起提供其服务端实现位于 chromium.ts。文档中给出的完整调用链是以http://localhost:9222或ws://.../devtools/browser/id形式给出端点到connectOverCDPPlaywright 内部通过urlToWSEndpoint把 HTTP 地址解析为 DevTools WebSocket 端点用WebSocketTransport建立连接chromium.ts中_connectOverCDPInternal的实现细节连接到进程后即获得 [Browser] 实例其默认上下文即 WebView2 宿主进程内的现有页面。由于连接的是运行中的真实宿主应用connectOverCDP模式下 Playwright 不会负责启动浏览器进程也不关闭应用本身——进程生命周期由你的测试代码管理。在 WebView2 应用中开启 CDP 远程调试端口要接收来自 Playwright 的 CDP 连接宿主应用需要在启动 WebView2 进程时携带--remote-debugging-port参数。文档给出了两种等价做法方式一设置环境变量在启动 WebView2 控件所属进程前设置WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS--remote-debugging-port9222方式二调用EnsureCoreWebView2Async在宿主代码中通过 EnsureCoreWebView2Async 传入CoreWebView2EnvironmentOptions.AdditionalBrowserArgumentsawait this.webView.EnsureCoreWebView2Async(await CoreWebView2Environment.CreateAsync(null, null, new CoreWebView2EnvironmentOptions() { AdditionalBrowserArguments --remote-debugging-port9222, })).ConfigureAwait(false);两种方式都会让 WebView2 以启用 Chrome DevTools Protocol 的方式启动从而允许 Playwright 自动化。9222只是示例端口任何未被占用的端口均可使用——下文各语言测试脚手架中会看到如何动态分配空闲端口。等待初始化完成连接建立前需要确保 WebView2 控件已完成初始化。可以订阅CoreWebView2InitializationCompleted事件this.webView.CoreWebView2InitializationCompleted (_, e) { if (e.IsSuccess) { Console.WriteLine(WebView2 initialized); } };WebView2 initialized这一行输出在后面的多语言测试脚手架中被当作「应用已就绪、可以连接」的握手信号。用各语言 connectOverCDP 连接已运行的 WebView2应用带着--remote-debugging-port运行后即可在各语言中连接。以 9222 端口为例const browser await playwright.chromium.connectOverCDP(http://localhost:9222); const context browser.contexts()[0]; const page context.pages()[0];Browser browser playwright.chromium().connectOverCDP(http://localhost:9222); BrowserContext context browser.contexts().get(0); Page page context.pages().get(0);browser await playwright.chromium.connect_over_cdp(http://localhost:9222) context browser.contexts[0] page context.pages[0]browser playwright.chromium.connect_over_cdp(http://localhost:9222) context browser.contexts[0] page context.pages[0]var browser await playwright.Chromium.ConnectOverCDPAsync(http://localhost:9222); var context browser.Contexts[0]; var page context.Pages[0];注意连接后取的是browser.contexts()[0]与context.pages()[0]即直接沿用 WebView2 进程中已存在的默认上下文与页面而不是像launch()那样新建。connectOverCDP 的可选参数围绕主流程仓库 API 契约文档给出了可在 options 中使用的附加参数参见 BrowserType.connectOverCDP 参数说明参数类型说明endpointURLstringCDP WebSocket 端点或 HTTP 地址如http://localhost:9222/或ws://127.0.0.1:9222/devtools/browser/idheadersObject连接请求携带的附加 HTTP 头可选isLocalboolean自 v1.58 起提供声明 Playwright 与 CDP 服务端运行在同一主机可启用依赖共享文件系统的优化slowMofloat将每个 Playwright 操作放慢指定的毫秒数便于肉眼观察执行过程默认 0timeoutfloat建立连接的最大等待毫秒数默认3000030 秒传0表示不超时noDefaultsboolean自 v1.60 起提供当 CDP 目标已有自身状态时避免 Playwright 注入默认覆盖artifactsDirpath自 v1.61 起提供指定下载、追踪等产物的目录从服务端实现看chromium.tsconnectOverCDP会对连接目标建立一个noDefaultViewport: true的持久化上下文——这符合 WebView2 场景真实桌面窗口的视口不应被 Playwright 改动。若未传isLocal: true远端连接会被标记为不与浏览器同机browser._isBrowserCollocatedWithServer false从而禁用依赖本地文件系统对齐的能力。默认 UA 缺失时Playwright 会自动注入自己的User-Agent见 chromium.ts。编写与运行测试并行隔离与用户数据目录为什么每个测试需要独立用户数据目录默认情况下所有 WebView2 控件实例共享同一个用户数据目录user data directory。如果多个测试并行运行它们会互相干扰抢用 Cookie、缓存、Profile 锁等。要避免冲突应为每个测试设置独立的WEBVIEW2_USER_DATA_FOLDER环境变量或通过WebView2.EnsureCoreWebView2Async指定确保各测试运行在各自的用户数据目录中。文档给出的通用模式是Playwright 把 WebView2 应用作为子进程启动、为它分配独立的用户数据目录、并直接把 [Page] 实例注入测试。这样对测试编写者来说使用方式与普通playwright/test的pagefixture 完全一致。TypeScript / JavaScript自定义 test fixtureswebView2Test.ts将browser、context、page三个 fixture 全部重定向到由 CDP 连接的 WebView2import { test as base } from playwright/test; import fs from fs; import os from os; import path from path; import childProcess from child_process; const EXECUTABLE_PATH path.join( __dirname, ../../webview2-app/bin/Debug/net8.0-windows/webview2.exe, ); export const test base.extend({ browser: async ({ playwright }, use, testInfo) { const cdpPort 10000 testInfo.workerIndex; // Make sure that the executable exists and is executable fs.accessSync(EXECUTABLE_PATH, fs.constants.X_OK); const userDataDir path.join( fs.realpathSync.native(os.tmpdir()), playwright-webview2-tests/user-data-dir-${testInfo.workerIndex}, ); const webView2Process childProcess.spawn(EXECUTABLE_PATH, [], { shell: true, env: { ...process.env, WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: --remote-debugging-port${cdpPort}, WEBVIEW2_USER_DATA_FOLDER: userDataDir, } }); await new Promisevoid(resolve webView2Process.stdout.on(data, data { if (data.toString().includes(WebView2 initialized)) resolve(); })); const browser await playwright.chromium.connectOverCDP(http://127.0.0.1:${cdpPort}); await use(browser); await browser.close(); childProcess.execSync(taskkill /pid ${webView2Process.pid} /T /F); fs.rmdirSync(userDataDir, { recursive: true }); }, context: async ({ browser }, use) { const context browser.contexts()[0]; await use(context); }, page: async ({ context }, use) { const page context.pages()[0]; await use(page); }, }); export { expect } from playwright/test;其核心设计点值得展开端口按 worker 隔离cdpPort 10000 testInfo.workerIndex不同并行 worker 用不同调试端口互不冲突握手协议通过childProcess.spawn启动 EXE 后监听stdout中的WebView2 initialized即上文CoreWebView2InitializationCompleted事件打印的输出才返回 browser fixture环境变量注入进程级注入WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS带出调试端口与WEBVIEW2_USER_DATA_FOLDER独立数据目录生命周期收尾use()结束后browser.close()断开 CDP、taskkill /pid ... /T /F结束进程树、再递归删除临时数据目录。对应的用例文件import { test, expect } from ./webView2Test; test(test WebView2, async ({ page }) { await page.goto(https://playwright.dev); const getStarted page.getByText(Get Started); await expect(getStarted).toBeVisible(); });用例中拿到的page即 WebView2 内嵌页面本身可继续使用 Playwright 的定位器locator与断言 API。JavaWebView2Process 帮助类 JUnit 5Java 侧先封装WebView2Process自动寻找空闲端口、创建独立临时数据目录、注入环境变量并启动进程同时阻塞等待应用打印WebView2 initializedpackage com.example; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class WebView2Process { public int cdpPort; private Path _dataDir; private Process _process; private Path _executablePath Path.of(../webview2-app/bin/Debug/net8.0-windows/webview2.exe); public WebView2Process() throws IOException { cdpPort nextFreePort(); _dataDir Files.createTempDirectory(pw-java-webview2-tests-); if (!Files.exists(_executablePath)) { throw new RuntimeException(Executable not found: _executablePath); } ProcessBuilder pb new ProcessBuilder().command(_executablePath.toAbsolutePath().toString()); MapString, String envMap pb.environment(); envMap.put(WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, --remote-debugging-port cdpPort); envMap.put(WEBVIEW2_USER_DATA_FOLDER, _dataDir.toString()); _process pb.start(); // wait until WebView2 initialized got printed BufferedReader reader new BufferedReader(new InputStreamReader(_process.getInputStream())); while (true) { String line reader.readLine(); if (line null) { throw new RuntimeException(WebView2 process exited); } if (line.contains(WebView2 initialized)) { break; } } } private static final AtomicInteger nextUnusedPort new AtomicInteger(9000); private static boolean available(int port) { try (ServerSocket ignored new ServerSocket(port)) { return true; } catch (IOException ignored) { return false; } } static int nextFreePort() { for (int i 0; i 100; i) { int port nextUnusedPort.getAndIncrement(); if (available(port)) { return port; } } throw new RuntimeException(Cannot find free port: nextUnusedPort.get()); } public void dispose() { _process.destroy(); try { _process.waitFor(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }细节available()用「尝试绑定ServerSocket」探测端口是否空闲失败即抛出Cannot find free port。dispose()会销毁进程并等待退出。JUnit 测试类在BeforeAll中一次性启动 WebView2 并建立 CDP 连接共享给类内所有测试AfterAll统一释放package com.example; import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.Locator; import com.microsoft.playwright.Page; import com.microsoft.playwright.Playwright; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; import java.io.IOException; public class TestExample { // Shared between all tests in this class. static WebView2Process webview2Process; static Playwright playwright; static Browser browser; static BrowserContext context; static Page page; BeforeAll static void launchBrowser() throws IOException { playwright Playwright.create(); webview2Process new WebView2Process(); browser playwright.chromium().connectOverCDP(http://127.0.0.1: webview2Process.cdpPort); context browser.contexts().get(0); page context.pages().get(0); } AfterAll static void closeBrowser() { webview2Process.dispose(); } Test public void shouldClickButton() { page.navigate(https://playwright.dev); Locator gettingStarted page.getByText(Get started); assertThat(gettingStarted).isVisible(); } }Pythonpytest fixturessession 作用域共享进程Python 侧在conftest.py中定义一套从「进程 → 浏览器 → 上下文 → 页面」的 fixture 链import os import socket import tempfile import pytest from pathlib import Path from playwright.sync_api import Playwright, Browser, BrowserContext import subprocess EXECUTABLE_PATH ( Path(__file__).parent / .. / webview2-app / bin / Debug / net8.0-windows / webview2.exe ) pytest.fixture(scopesession) def data_dir(): with tempfile.TemporaryDirectory( prefixplaywright-webview2-tests, ignore_cleanup_errorsTrue ) as tmpdirname: yield tmpdirname pytest.fixture(scopesession) def webview2_process_cdp_port(data_dir: str): cdp_port _find_free_port() process subprocess.Popen( [EXECUTABLE_PATH], env{ **dict(os.environ), WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: f--remote-debugging-port{cdp_port}, WEBVIEW2_USER_DATA_FOLDER: data_dir, }, stdoutsubprocess.PIPE, stderrsubprocess.STDOUT, universal_newlinesTrue, ) while True: line process.stdout.readline() if WebView2 initialized in line: break yield cdp_port process.terminate() pytest.fixture(scopesession) def browser(playwright: Playwright, webview2_process_cdp_port: int): browser playwright.chromium.connect_over_cdp( fhttp://127.0.0.1:{webview2_process_cdp_port} ) yield browser pytest.fixture(scopefunction) def context(browser: Browser): context browser.contexts[0] yield context pytest.fixture(scopefunction) def page(context: BrowserContext): page context.pages[0] yield page def _find_free_port(port9000, max_port65535): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) while port max_port: try: sock.bind((, port)) sock.close() return port except OSError: port 1 raise IOError(no free ports)对应用例from playwright.sync_api import Page, expect def test_webview2(page: Page): page.goto(https://playwright.dev) get_started page.get_by_text(Get Started) expect(get_started).to_be_visible()此处 scope 选择的取舍同样值得注意进程、端口、浏览器均为session级整个测试会话只启动一次 WebView2、一个数据目录而context、page为function级为每个用例提供独立的页面引用。C#继承测试基类MSTest / NUnit / xUnit / xUnit v3C# 侧通过编写一个继承PlaywrightTest的WebView2Test基类把「启动 WebView2 进程 连接 CDP 暴露 Browser/Context/Page」固化为测试生命周期逻辑。.NET平台下该进程方案同样依赖 Windows 的webview2.exe位于..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe并使用10000 WorkerIndex实现按 worker 隔离的调试端口。四套测试框架仅初始化/清理注解与继承命名空间不同主体逻辑一致。MSTest基类using System.Diagnostics; using Microsoft.Playwright; using Microsoft.Playwright.MSTest; namespace PlaywrightTests; public class WebView2Test : PlaywrightTest { public IBrowser Browser { get; internal set; } null!; public IBrowserContext Context { get; internal set; } null!; public IPage Page { get; internal set; } null!; private Process? _webView2Process null; private string _userDataDir null!; private string _executablePath Path.Join(Directory.GetCurrentDirectory(), ..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe); [TestInitialize] public async Task BrowserTestInitialize() { var cdpPort 10000 WorkerIndex; Assert.IsTrue(File.Exists(_executablePath), Make sure that the executable exists); _userDataDir Path.Join(Path.GetTempPath(), $playwright-webview2-tests/user-data-dir-{WorkerIndex}); // WebView2 does some lazy cleanups on shutdown so we cant clean it up after each test if (Directory.Exists(_userDataDir)) { Directory.Delete(_userDataDir, true); } _webView2Process Process.Start(new ProcessStartInfo(_executablePath) { EnvironmentVariables { [WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS] $--remote-debugging-port{cdpPort}, [WEBVIEW2_USER_DATA_FOLDER] _userDataDir, }, RedirectStandardOutput true, }); while (!_webView2Process!.HasExited) { var output await _webView2Process!.StandardOutput.ReadLineAsync(); if (_webView2Process!.HasExited) { throw new Exception(WebView2 process exited unexpectedly); } if (output ! null output.Contains(WebView2 initialized)) { break; } } var cdpAddress $http://127.0.0.1:{cdpPort}; Browser await Playwright.Chromium.ConnectOverCDPAsync(cdpAddress); Context Browser.Contexts[0]; Page Context.Pages[0]; } [TestCleanup] public async Task BrowserTestCleanup() { _webView2Process!.Kill(true); await Browser.CloseAsync(); } }using Microsoft.Playwright; using Microsoft.Playwright.MSTest; namespace PlaywrightTests; [TestClass] public class ExampleTest : WebView2Test { [TestMethod] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() { await Page.GotoAsync(https://playwright.dev); var getStarted Page.GetByText(Get Started); await Expect(getStarted).ToBeVisibleAsync(); } }NUnit基类中把[TestInitialize]/[TestCleanup]换成[SetUp]/[TearDown]继承Microsoft.Playwright.NUnit.PlaywrightTest用例侧给测试类标注[Parallelizable(ParallelScope.Self)]与[TestFixture]测试方法使用[Test]并启用using NUnit.Framework;。using System.Diagnostics; using Microsoft.Playwright; using Microsoft.Playwright.NUnit; using NUnit.Framework; namespace PlaywrightTests; public class WebView2Test : PlaywrightTest { public IBrowser Browser { get; internal set; } null!; public IBrowserContext Context { get; internal set; } null!; public IPage Page { get; internal set; } null!; private Process? _webView2Process null; private string _userDataDir null!; private string _executablePath Path.Join(Directory.GetCurrentDirectory(), ..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe); [SetUp] public async Task BrowserTestSetUp() { var cdpPort 10000 WorkerIndex; Assert.That(File.Exists(_executablePath), Is.True, Make sure that the executable exists); _userDataDir Path.Join(Path.GetTempPath(), $playwright-webview2-tests/user-data-dir-{WorkerIndex}); // WebView2 does some lazy cleanups on shutdown so we cant clean it up after each test if (Directory.Exists(_userDataDir)) { Directory.Delete(_userDataDir, true); } _webView2Process Process.Start(new ProcessStartInfo(_executablePath) { EnvironmentVariables { [WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS] $--remote-debugging-port{cdpPort}, [WEBVIEW2_USER_DATA_FOLDER] _userDataDir, }, RedirectStandardOutput true, }); while (!_webView2Process!.HasExited) { var output await _webView2Process!.StandardOutput.ReadLineAsync(); if (_webView2Process!.HasExited) { throw new Exception(WebView2 process exited unexpectedly); } if (output ! null output.Contains(WebView2 initialized)) { break; } } var cdpAddress $http://127.0.0.1:{cdpPort}; Browser await Playwright.Chromium.ConnectOverCDPAsync(cdpAddress); Context Browser.Contexts[0]; Page Context.Pages[0]; } [TearDown] public async Task BrowserTestTearDown() { _webView2Process!.Kill(true); await Browser.CloseAsync(); } }using Microsoft.Playwright; using Microsoft.Playwright.NUnit; using NUnit.Framework; namespace PlaywrightTests; [Parallelizable(ParallelScope.Self)] [TestFixture] public class ExampleTest : WebView2Test { [Test] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() { await Page.GotoAsync(https://playwright.dev); var getStarted Page.GetByText(Get Started); await Expect(getStarted).ToBeVisibleAsync(); } }xUnit基类改用InitializeAsync/DisposeAsync覆写继承Microsoft.Playwright.Xunit.PlaywrightTest基类内先await base.InitializeAsync();再建立 CDP 连接DisposeAsync末尾恢复调用基类清理用例方法使用[Fact]。using System.Diagnostics; using Microsoft.Playwright; using Microsoft.Playwright.Xunit; namespace PlaywrightTests; public class WebView2Test : PlaywrightTest { public IBrowser Browser { get; internal set; } null!; public IBrowserContext Context { get; internal set; } null!; public IPage Page { get; internal set; } null!; private Process? _webView2Process null; private string _userDataDir null!; private string _executablePath Path.Join(Directory.GetCurrentDirectory(), ..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe); public override async Task InitializeAsync() { await base.InitializeAsync(); var cdpPort 10000 WorkerIndex; Assert.True(File.Exists(_executablePath), Make sure that the executable exists); _userDataDir Path.Join(Path.GetTempPath(), $playwright-webview2-tests/user-data-dir-{WorkerIndex}); // WebView2 does some lazy cleanups on shutdown so we cant clean it up after each test if (Directory.Exists(_userDataDir)) { Directory.Delete(_userDataDir, true); } _webView2Process Process.Start(new ProcessStartInfo(_executablePath) { EnvironmentVariables { [WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS] $--remote-debugging-port{cdpPort}, [WEBVIEW2_USER_DATA_FOLDER] _userDataDir, }, RedirectStandardOutput true, }); while (!_webView2Process!.HasExited) { var output await _webView2Process!.StandardOutput.ReadLineAsync(); if (_webView2Process!.HasExited) { throw new Exception(WebView2 process exited unexpectedly); } if (output ! null output.Contains(WebView2 initialized)) { break; } } var cdpAddress $http://127.0.0.1:{cdpPort}; Browser await Playwright.Chromium.ConnectOverCDPAsync(cdpAddress); Context Browser.Contexts[0]; Page Context.Pages[0]; } public override async Task DisposeAsync() { _webView2Process!.Kill(true); await Browser.CloseAsync(); await base.DisposeAsync(); } }using Microsoft.Playwright; using Microsoft.Playwright.Xunit; namespace PlaywrightTests; public class ExampleTest : WebView2Test { [Fact] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() { await Page.GotoAsync(https://playwright.dev); var getStarted Page.GetByText(Get Started); await Expect(getStarted).ToBeVisibleAsync(); } }xUnit v3仅命名空间变为Microsoft.Playwright.Xunit.v3基类结构与 xUnit 相同using System.Diagnostics; using Microsoft.Playwright; using Microsoft.Playwright.Xunit.v3; namespace PlaywrightTests; public class WebView2Test : PlaywrightTest { public IBrowser Browser { get; internal set; } null!; public IBrowserContext Context { get; internal set; } null!; public IPage Page { get; internal set; } null!; private Process? _webView2Process null; private string _userDataDir null!; private string _executablePath Path.Join(Directory.GetCurrentDirectory(), ..\..\..\..\webview2-app\bin\Debug\net8.0-windows\webview2.exe); public override async Task InitializeAsync() { await base.InitializeAsync(); var cdpPort 10000 WorkerIndex; Assert.True(File.Exists(_executablePath), Make sure that the executable exists); _userDataDir Path.Join(Path.GetTempPath(), $playwright-webview2-tests/user-data-dir-{WorkerIndex}); // WebView2 does some lazy cleanups on shutdown so we cant clean it up after each test if (Directory.Exists(_userDataDir)) { Directory.Delete(_userDataDir, true); } _webView2Process Process.Start(new ProcessStartInfo(_executablePath) { EnvironmentVariables { [WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS] $--remote-debugging-port{cdpPort}, [WEBVIEW2_USER_DATA_FOLDER] _userDataDir, }, RedirectStandardOutput true, }); while (!_webView2Process!.HasExited) { var output await _webView2Process!.StandardOutput.ReadLineAsync(); if (_webView2Process!.HasExited) { throw new Exception(WebView2 process exited unexpectedly); } if (output ! null output.Contains(WebView2 initialized)) { break; } } var cdpAddress $http://127.0.0.1:{cdpPort}; Browser await Playwright.Chromium.ConnectOverCDPAsync(cdpAddress); Context Browser.Contexts[0]; Page Context.Pages[0]; } public override async Task DisposeAsync() { _webView2Process!.Kill(true); await Browser.CloseAsync(); await base.DisposeAsync(); } }using Microsoft.Playwright; using Microsoft.Playwright.Xunit.v3; namespace PlaywrightTests; public class ExampleTest : WebView2Test { [Fact] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() { await Page.GotoAsync(https://playwright.dev); var getStarted Page.GetByText(Get Started); await Expect(getStarted).ToBeVisibleAsync(); } }C# 基类中有一条值得注意的注释WebView2 在关闭时会做惰性清理因此不要在每次测试后立刻删除数据目录——这也解释了为何各语言脚手架把数据目录的创建、回收安排在 worker/session 维度而非单个用例维度。调试 WebView2 应用与测试WebView2 控件本身支持三种打开 DevTools 的方式在控件内右键打开上下文菜单选择Inspect直接按键盘F12调用 WebView2.CoreWebView2.OpenDevToolsWindow 以编程方式弹出 DevTools 窗口。对于 Playwright 测试自身的调试可查阅 Playwright 的 Debugging 指南。文档还专门提示由于用例通过connectOverCDP附加到真实 WebView2 进程调试器、Trace 查看器、codegen 等在普通 Chromium 场景下可用的工具与技巧在保证宿主应用不退出进程生命周期由 fixture 持有的前提下同样适用。结语从手动联调到多语言自动化测试纵观全文WebView2 自动化测试的核心心智模型只有三步1让宿主应用带--remote-debugging-port启动环境变量或AdditionalBrowserArguments2测试脚手架以子进程方式拉起应用、分配独立WEBVIEW2_USER_DATA_FOLDER并等待WebView2 initialized握手信号3用chromium.connectOverCDP附加到进程并复用其首个上下文/页面。无论你身处 TypeScript、Java、Python 还是 C# 技术栈都可以用官方文档 webview2.md 中这几套几乎同构的模板快速把 WebView2 场景并入现有的 Playwright 测试体系。【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

2026/9/7 23:31:46

Yolov8目标检测训练实战:从数据准备到模型部署

简介:YOLOv8是目前主流的高效目标检测算法,在实时性与准确率之间取得了良好平衡。这份PDF文档面向需要在Ubuntu 22.04下搭建深度学习环境,并用自定义数据集训练YOLOv8实例分割模型的研究者与开发者,系统整理了从环境部署到模型落地…

2026/9/7 23:26:46

Python实战:手机销售多维分析系统与交互式仪表盘设计

简介:面向具备Python编程基础、熟悉pandas等数据处理库的数据分析与商业智能人员,这是一份基于Python的手机销售多维度可视化系统的docx项目介绍,重点解决多源异构销售数据整合清洗、多维分析、交互式仪表盘设计以及经营决策支撑等实际问题。…

2026/9/7 23:26:46

Python+Pandas+Plotly Dash:构建手机销售多维分析交互式仪表盘

简介:面向具备Python基础、从事数据分析或商业智能的研发人员,这份手机销售数据可视化项目介绍文档聚焦多源销售数据整合清洗、多维度分析视图与Dash交互式仪表盘搭建,针对数据整合难度大、交互性能与体验平衡、技术复杂度与可维护性等挑战给…

2026/9/8 1:41:58

基于STM32的红外循迹避障小车完整实战与调试指南

简介:一套基于STM32的红外循迹避障小车完整C程序工程,适合嵌入式学习者、电子竞赛备赛者及智能小车爱好者作为实战参考。以STM32为主控,结合红外循迹与避障传感器,实现路径检测、障碍识别和自动转向控制,涵盖SysTick延…

2026/9/8 1:41:58

7.3.2.3.2 选择一个 Preamble 和 PRACH 的时频时机

本节课程视频 7.3.2.3.2.1 一次 Msg1 到底需要确定什么? 上一小节讨论了初始化随机接入如何在手机内部被触发。本节从触发之后继续向下看:MAC/PHY需要把“我要做一次随机接入”变成一个具体的空口发送动作。这个动作至少需要回答两个问题:…

2026/9/8 1:41:58

多模态图像融合工具实战:从配准到金字塔融合

简介:这款 IROM_Fusing_Tool 源码包是针对 Samsung S3C6410 嵌入式处理器的专业 EBOOT 烧写工具,用于解决 SD 卡启动流程中引导程序更新困难的问题,主要面向嵌入式开发工程师、设备调试人员以及学习底层固件机制的开发者。EBOOT 作为设备启动…

2026/9/8 1:41:58

ComfyUI+豆包+即梦AI:零基础制作人物一致AI漫剧完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/8 1:41:58

SQL Server脚本导出导入:多表数据迁移的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/8 1:36:58

pytest高级实战:从基础断言到智能断言、fixture与接口自动化

写这篇东西的起因,是我在一个pytest项目里被一份测试报告逼疯了:三百多条全绿,但业务联调时接口数据错得离谱。那之后我花了很长时间去研究pytest到底能做什么,才发现大多数团队的用法停留在"能跑就行"的阶段。本文不是…

2026/9/7 0:47:43

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/7 0:14:19

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/7 0:14:17

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/8 0:01:49

踩多轮坑才跑通|OpenClaw 3.1.0 双平台本地 AI 自动化搭建实操实录

🔹 工具简述 OpenClaw 是一款备受开发者与办公人群青睐的开源本地智能工具,凭借离线本地运行、可视化图形面板、全流程自主任务处理三大核心特点,积累了众多忠实用户。与普通对话类 AI 产品不同,它能够直接调用电脑的软硬件操作权…

2026/9/8 0:01:50

拒绝复杂命令行,Hermes Agent 一键包快速解锁智能办公能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…