c#端到端测试应使用microsoft.playwright,手动下载浏览器二进制(如playwright install chromium),避免混淆nuget包与浏览器运行时;每个测试需独立创建ibrowsercontext,优先使用语义化等待和拦截api,截图前确保元素加载并配置渲染参数。

直接上结论:C#端到端测试用 Microsoft.Playwright,别碰 playwright(那是 Node.js 包)、也别用已停更的 PlaywrightSharp;浏览器二进制必须手动下载,await Playwright.CreateAsync() 不会自动装浏览器,只调用它必报 PlaywrightException: Failed to launch browser。
安装 Microsoft.Playwright + 下载浏览器二进制
很多人卡在第一步就失败,根本原因是混淆了“NuGet 包”和“浏览器运行时”的关系。NuGet 只提供 .NET 绑定,不带任何浏览器可执行文件。
- 执行
dotnet add package Microsoft.Playwright—— 这步只是加引用,没浏览器 - 必须额外运行命令下载浏览器:
playwright install chromium(推荐)或playwright install firefox webkit - 若提示
command not found: playwright,说明 CLI 工具未全局安装,改用:dotnet tool install -g Microsoft.Playwright.CLI,再重试 - CI/CD 环境中建议加
--with-deps:例如playwright install chromium --with-deps,避免 Linux 上缺libglib或libnss导致启动失败 - Docker 用户注意:基础镜像不能用
alpine,musl 不被支持;推荐mcr.microsoft.com/dotnet/sdk:8.0-jammy
BrowserContext 必须按测试粒度隔离
复用 IBrowserContext 是常见误操作源头,会导致 cookie、localStorage、路由状态、网络拦截规则全部污染。
- 错误写法:把
browser.NewContextAsync()结果存为类字段,在多个测试方法里共用 - 正确做法:每个测试方法内独立创建
context = await browser.NewContextAsync(),再调context.NewPageAsync() - 若需保持登录态(比如跳过重复登录),应将
IBrowserContext提升为测试类生命周期内的单例,并显式管理其销毁时机(如[TestCleanup]中await context.CloseAsync()) - 跨域名跳转(如从
login.example.com到app.example.com)会清空部分存储,此时应为每个主域名单独建IBrowserContext
WaitForURLAsync 和 IsVisibleAsync 的典型误用
断言逻辑松散是 E2E 测试 flaky(不稳定)的主因,仅靠 URL 或 DOM 文本字符串匹配远远不够。
-
page.WaitForURLAsync("/dashboard")默认 30 秒超时,但页面可能跳转成功却未渲染完内容;务必显式设超时:new PageWaitForURLOptions { Timeout = 10000 } - 登录后验证“欢迎语”,优先用
await page.GetByText("Welcome,").IsVisibleAsync(),而不是Assert.Contains("Welcome", await page.ContentAsync())—— 后者拿到的是初始 HTML,常含占位符或未 hydrate 的 SSR 内容 - 需要确认 API 响应生效?用
page.RouteAsync("**/api/profile", route => route.FulfillAsync(...))拦截并伪造响应,绕过真实后端依赖 - 等待元素出现别用
WaitForTimeoutAsync(2000),它不感知状态,应改用await page.WaitForSelectorAsync("header nav a.active")这类语义化等待
截图黑屏 / 空文件的根因与修复
page.ScreenshotAsync() 返回黑图或空文件,90% 是 headless 渲染问题,不是代码 bug。
- 默认
Headless = true下,Chromium 会禁用 GPU 加速和某些字体渲染路径;若页面含<canvas></canvas>、SVG 动画或自定义字体,极易黑屏 - 临时调试可改用有头模式:
new BrowserTypeLaunchOptions { Headless = false },但生产环境禁用 - 更稳妥的方案是在 launch 参数中加入渲染兜底选项:
Args = new[] { "--disable-gpu", "--no-sandbox", "--font-render-hinting=none" } - 截图前必须等关键区域加载完成,例如:
await page.WaitForSelectorAsync("main"); await page.ScreenshotAsync(new() { Path = "result.png" }) - 全屏截图(
FullPage = true)易因滚动高度计算不准导致截断,可视区截图(默认)更可靠;真要全页,先await page.EvaluateAsync("window.scrollTo(0, document.body.scrollHeight)")
真正难的不是写第一行 await page.ClickAsync(),而是理解 IBrowserContext 的边界、page 的生命周期、以及 headless 渲染的隐式约束——这些点不厘清,测试脚本越写越脆,失败时连日志都看不出哪一步断掉了。










