mcpField
D
dotnet-playwright

dotnet-playwright

github.com/wshaddix测试与质量8682 次安装35 周前更新
代码仓库

关于 dotnet-playwright

dotnet-playwright 是面向 .NET 开发者的 Playwright 浏览器自动化与端到端测试技能,涵盖从交互和断言到故障排查的实践。它还介绍多浏览器测试、CI 浏览器缓存及代码生成后的完善方式。

主要功能

  1. 01浏览器生命周期与页面交互
  2. 02Playwright 断言与网络拦截
  3. 03Chromium、Firefox、WebKit 测试
  4. 04CI 浏览器二进制缓存
  5. 05Trace Viewer 故障排查
  6. 06代码生成与测试脚手架

适用场景

  1. 01为 .NET 应用编写端到端测试
  2. 02模拟 API 响应并验证页面
  3. 03在 CI 中运行多浏览器测试
  4. 04通过执行轨迹排查测试失败

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-playwright

安装后在 Claude Code 中输入/dotnet-playwright确认已加载。

详细介绍

dotnet-playwright

适用于 .NET 的 Playwright:浏览器自动化和端到端测试。涵盖浏览器生命周期管理、页面交互、断言、CI 浏览器二进制文件缓存、用于调试故障的 Trace Viewer,以及用于快速搭建测试框架的 codegen。

**版本假设:**Playwright 1.40+ for .NET,.NET 8.0+ 基线。Playwright 支持 Chromium、Firefox 和 WebKit 浏览器。

**范围之外:**通用 UI 测试模式(页面对象模型、选择器、等待策略)请参阅 [skill:dotnet-ui-testing-core]。测试策略(何时使用 E2E 测试、单元测试或集成测试)请参阅 [skill:dotnet-testing-strategy]。测试项目脚手架由 [skill:dotnet-add-testing] 负责。

**前置条件:**通过 [skill:dotnet-add-testing] 搭建测试项目脚手架,并引用 Playwright 包。通过 pwsh bin/Debug/net8.0/playwright.ps1 install 或 dotnet tool run playwright install 安装浏览器。

交叉引用:[skill:dotnet-ui-testing-core] 介绍页面对象模型和选择器策略,[skill:dotnet-testing-strategy] 用于判断何时适合使用 E2E 测试。


包设置

<PackageReference Include="Microsoft.Playwright" Version="1.*" />

<PackageReference Include="Microsoft.Playwright.Xunit" Version="1.*" />


安装浏览器

运行测试前,Playwright 需要下载浏览器二进制文件:

# After building the test project:
pwsh bin/Debug/net8.0/playwright.ps1 install

# Or install specific browsers:
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
pwsh bin/Debug/net8.0/playwright.ps1 install firefox

# Using dotnet tool:
dotnet tool install --global Microsoft.Playwright.CLI
playwright install

基本测试结构

使用 Playwright xUnit 基类
using Microsoft.Playwright;
using Microsoft.Playwright.Xunit;

// PageTest provides Page, Browser, BrowserContext, and Playwright properties
public class HomePageTests : PageTest
{
    [Fact]
    public async Task HomePage_Title_ContainsAppName()
    {
        await Page.GotoAsync("https://localhost:5001");

        await Expect(Page).ToHaveTitleAsync(new Regex("My App"));
    }

    [Fact]
    public async Task HomePage_NavLinks_AreVisible()
    {
        await Page.GotoAsync("https://localhost:5001");

        var nav = Page.Locator("nav");
        await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "Home" }))
            .ToBeVisibleAsync();
        await Expect(nav.GetByRole(AriaRole.Link, new() { Name = "About" }))
            .ToBeVisibleAsync();
    }
}
手动设置(不使用基类)
public class ManualSetupTests : IAsyncLifetime
{
    private IPlaywright _playwright = null!;
    private IBrowser _browser = null!;
    private IBrowserContext _context = null!;
    private IPage _page = null!;

    public async ValueTask InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true
        });
        _context = await _browser.NewContextAsync(new BrowserNewContextOptions
        {
            ViewportSize = new ViewportSize { Width = 1280, Height = 720 },
            Locale = "en-US"
        });
        _page = await _context.NewPageAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await _page.CloseAsync();
        await _context.CloseAsync();
        await _browser.CloseAsync();
        _playwright.Dispose();
    }

    [Fact]
    public async Task Login_ValidUser_RedirectsToDashboard()
    {
        await _page.GotoAsync("https://localhost:5001/login");

        await _page.FillAsync("[data-testid='email']", "[email protected]");
        await _page.FillAsync("[data-testid='password']", "P@ssw0rd!");
        await _page.ClickAsync("[data-testid='login-btn']");

        await Expect(_page).ToHaveURLAsync(new Regex("/dashboard"));
    }
}

定位器和交互

推荐的定位器策略
// BEST: Role-based (accessible and semantic)
var submitBtn = Page.GetByRole(AriaRole.Button, new() { Name = "Submit Order" });

// GOOD: Test ID (stable, explicit)
var emailInput = Page.Locator("[data-testid='email-input']");

// GOOD: Label text (user-visible, accessible)
var nameField = Page.GetByLabel("Full Name");

// GOOD: Placeholder (user-visible)
var searchBox = Page.GetByPlaceholder("Search products...");

// AVOID: CSS class (fragile, changes with styling)
var card = Page.Locator(".card-primary");

// AVOID: XPath (brittle, hard to read)
var cell = Page.Locator("//table/tbody/tr[1]/td[2]");
常见交互
// Text input
await Page.FillAsync("[data-testid='name']", "Alice Johnson");

// Click
await Page.ClickAsync("[data-testid='submit']");

// Select dropdown
await Page.SelectOptionAsync("[data-testid='country']", "US");

// Checkbox / radio
await Page.CheckAsync("[data-testid='agree-terms']");

// File upload
await Page.SetInputFilesAsync("[data-testid='avatar']", "testdata/photo.jpg");

// Keyboard
await Page.Keyboard.PressAsync("Enter");
await Page.Keyboard.TypeAsync("search query");

// Hover (for dropdowns, tooltips)
await Page.HoverAsync("[data-testid='user-menu']");
断言(Expect API)

Playwright 断言会自动重试,直到条件满足或超时:

// Element visibility
await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
await Expect(Page.Locator("[data-testid='spinner']")).ToBeHiddenAsync();

// Text content
await Expect(Page.Locator("[data-testid='total']")).ToHaveTextAsync("$99.99");
await Expect(Page.Locator("[data-testid='status']")).ToContainTextAsync("Completed");

// Attribute
await Expect(Page.Locator("[data-testid='submit']")).ToBeEnabledAsync();
await Expect(Page.Locator("[data-testid='email']")).ToHaveValueAsync("[email protected]");

// Page-level
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));
await Expect(Page).ToHaveTitleAsync("Order Details - My App");

// Count
await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(5);

网络拦截

模拟 API 响应
[Fact]
public async Task OrderList_WithMockedApi_DisplaysOrders()
{
    // Intercept API calls and return mock data
    await Page.RouteAsync("**/api/orders", async route =>
    {
        var json = JsonSerializer.Serialize(new[]
        {
            new { Id = 1, CustomerName = "Alice", Total = 99.99 },
            new { Id = 2, CustomerName = "Bob", Total = 149.50 }
        });
        await route.FulfillAsync(new RouteFulfillOptions
        {
            Status = 200,
            ContentType = "application/json",
            Body = json
        });
    });

    await Page.GotoAsync("https://localhost:5001/orders");

    await Expect(Page.Locator("[data-testid='order-row']")).ToHaveCountAsync(2);
}
等待网络请求
[Fact]
public async Task CreateOrder_SubmitForm_WaitsForApiResponse()
{
    await Page.GotoAsync("https://localhost:5001/orders/new");

    await Page.FillAsync("[data-testid='customer']", "Alice");
    await Page.FillAsync("[data-testid='amount']", "99.99");

    // Wait for the API call triggered by form submission
    var responseTask = Page.WaitForResponseAsync(
        response => response.Url.Contains("/api/orders") && response.Status == 201);

    await Page.ClickAsync("[data-testid='submit']");

    var response = await responseTask;
    Assert.Equal(201, response.Status);
}

CI 浏览器缓存

每次 CI 运行都下载浏览器二进制文件会很慢(500MB+)。缓存这些文件可以加快构建速度。

GitHub Actions 缓存
# .github/workflows/e2e-tests.yml
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Build
        run: dotnet build tests/MyApp.E2E/

      - name: Cache Playwright browsers
        id: playwright-cache
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('tests/MyApp.E2E/MyApp.E2E.csproj') }}

      - name: Install Playwright browsers
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps

      - name: Install Playwright system deps
        if: steps.playwright-cache.outputs.cache-hit == 'true'
        run: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps

      - name: Run E2E tests
        run: dotnet test tests/MyApp.E2E/
Azure DevOps 缓存
# azure-pipelines.yml
steps:
  - task: Cache@2
    inputs:
      key: 'playwright | "$(Agent.OS)" | tests/MyApp.E2E/MyApp.E2E.csproj'
      path: $(HOME)/.cache/ms-playwright
      restoreKeys: |
        playwright | "$(Agent.OS)"
      cacheHitVar: PLAYWRIGHT_CACHE_RESTORED
    displayName: Cache Playwright browsers

  - script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install --with-deps
    condition: ne(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
    displayName: Install Playwright browsers

  - script: pwsh tests/MyApp.E2E/bin/Debug/net8.0/playwright.ps1 install-deps
    condition: eq(variables.PLAYWRIGHT_CACHE_RESTORED, 'true')
    displayName: Install Playwright system deps (cached browsers)

  - script: dotnet test tests/MyApp.E2E/
    displayName: Run E2E tests
缓存键策略

缓存键应包含:

  • **操作系统:**浏览器二进制文件因平台而异
  • **项目文件哈希:**Playwright 版本决定浏览器版本;更改包版本会使缓存失效
  • **回退键:**项目文件发生更改时,允许部分恢复缓存

Trace Viewer

Playwright 的 Trace Viewer 会完整记录测试执行过程,以便调试故障。每条跟踪都包含屏幕截图、DOM 快照、网络日志和控制台输出。

启用跟踪
public class TracedTests : IAsyncLifetime
{
    private IPlaywright _playwright = null!;
    private IBrowser _browser = null!;
    private IBrowserContext _context = null!;

    public IPage Page { get; private set; } = null!;

    public async ValueTask InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync();
        _context = await _browser.NewContextAsync();

        // Start tracing before each test
        await _context.Tracing.StartAsync(new TracingStartOptions
        {
            Screenshots = true,
            Snapshots = true,
            Sources = true
        });

        Page = await _context.NewPageAsync();
    }

    public async ValueTask DisposeAsync()
    {
        // Save trace on failure (check test result in xUnit requires custom wrapper)
        await _context.Tracing.StopAsync(new TracingStopOptions
        {
            Path = Path.Combine("test-results", "traces",
                $"trace-{DateTime.UtcNow:yyyyMMdd-HHmmss}.zip")
        });

        await Page.CloseAsync();
        await _context.CloseAsync();
        await _browser.CloseAsync();
        _playwright.Dispose();
    }
}
查看跟踪
# Open trace file in browser
pwsh bin/Debug/net8.0/playwright.ps1 show-trace test-results/traces/trace-20260101-120000.zip

# Or use the online trace viewer
# Upload the .zip to https://trace.playwright.dev/
仅在失败时跟踪

仅在测试失败时保存跟踪,以减少存储占用:

// In a custom test class or middleware
public async Task RunWithTrace(Func<IPage, Task> testAction, string testName)
{
    await _context.Tracing.StartAsync(new TracingStartOptions
    {
        Screenshots = true,
        Snapshots = true,
        Sources = true
    });

    try
    {
        await testAction(Page);
        // Test passed -- discard trace
        await _context.Tracing.StopAsync();
    }
    catch
    {
        // Test failed -- save trace for debugging
        await _context.Tracing.StopAsync(new TracingStopOptions
        {
            Path = $"test-results/traces/{testName}.zip"
        });
        throw;
    }
}

Codegen

Playwright 的代码生成器会记录浏览器交互并生成测试代码。可用它快速搭建测试框架,然后优化生成的代码。

运行 Codegen
# Open codegen with your app URL
pwsh bin/Debug/net8.0/playwright.ps1 codegen https://localhost:5001

# With specific browser
pwsh bin/Debug/net8.0/playwright.ps1 codegen --browser firefox https://localhost:5001

# With device emulation
pwsh bin/Debug/net8.0/playwright.ps1 codegen --device "iPhone 15" https://localhost:5001

# With saved authentication state
pwsh bin/Debug/net8.0/playwright.ps1 codegen --save-storage auth.json https://localhost:5001
Codegen 最佳实践
  1. **将 codegen 用作起点,**而非最终测试。生成的代码通常使用脆弱的选择器,且缺少适当的断言。
  2. 生成代码后立即将生成的选择器替换为 data-testid 或基于角色的定位器。
  3. **添加有意义的断言。**Codegen 会记录操作,但不知道需要验证什么。添加 Expect() 调用来验证预期结果。
  4. **从生成的代码中提取页面对象。**将相关交互归纳到页面对象方法中。
Codegen 优化前后对比
// GENERATED by codegen (fragile, no assertions):
await page.GotoAsync("https://localhost:5001/orders");
await page.Locator("#root > div > main > div:nth-child(2) > button").ClickAsync();
await page.GetByPlaceholder("Customer name").FillAsync("Alice");
await page.GetByPlaceholder("Amount").FillAsync("99.99");
await page.Locator("form > button[type='submit']").ClickAsync();

// REFINED (stable selectors, proper assertions):
await Page.GotoAsync("https://localhost:5001/orders");
await Page.ClickAsync("[data-testid='new-order-btn']");
await Page.FillAsync("[data-testid='customer-name']", "Alice");
await Page.FillAsync("[data-testid='amount']", "99.99");
await Page.ClickAsync("[data-testid='submit-order']");

await Expect(Page.Locator("[data-testid='success-toast']"))
    .ToBeVisibleAsync();
await Expect(Page).ToHaveURLAsync(new Regex("/orders/\\d+"));

多浏览器测试

在多个浏览器上运行测试
// Using Playwright xUnit base class with environment variable
// Set BROWSER=chromium|firefox|webkit via CLI or CI config
public class CrossBrowserTests : PageTest
{
    [Fact]
    public async Task OrderFlow_WorksAcrossBrowsers()
    {
        // This test runs in whichever browser BROWSER env var specifies
        await Page.GotoAsync("https://localhost:5001/orders/new");
        await Page.FillAsync("[data-testid='customer']", "Alice");
        await Page.ClickAsync("[data-testid='submit']");

        await Expect(Page.Locator("[data-testid='success']")).ToBeVisibleAsync();
    }
}
# Run tests in each browser
BROWSER=chromium dotnet test
BROWSER=firefox dotnet test
BROWSER=webkit dotnet test
CI 矩阵策略
# GitHub Actions matrix for multi-browser
strategy:
  matrix:
    browser: [chromium, firefox, webkit]
steps:
  - name: Run E2E tests
    run: dotnet test tests/MyApp.E2E/
    env:
      BROWSER: ${{ matrix.browser }}

核心原则

  • 使用 Playwright 断言(Expect),而不是原始 xUnit Assert。 Playwright 断言会自动重试,并支持配置超时时间,从而避免不稳定的时序问题。
  • 在 CI 中缓存浏览器二进制文件。 每次运行都下载 500MB 以上的浏览器会浪费时间和带宽。请按操作系统和 Playwright 版本进行缓存。
  • 启用 trace viewer 来调试 CI 故障。 Trace 会捕获复现故障所需的一切信息,无需重新运行测试。
  • 先用 codegen 搭建测试,再进行完善。 生成的代码能让你快速起步;手动完善则能让测试更易维护。
  • 优先使用基于角色或 data-testid 的定位器,而不是 CSS 类或 XPath。完整的选择器优先级指南请参阅 [skill:dotnet-ui-testing-core]。

Agent 注意事项

  1. 添加 Playwright 包后,别忘了安装浏览器。 NuGet 包不包含浏览器二进制文件。构建后运行安装脚本。
  2. 不要使用 Task.Delay 来等待。 Playwright 的自动等待和 Expect 断言会自动处理时序。添加延迟会让测试变慢,而且仍然不稳定。
  3. 不要硬编码 localhost 端口。 使用配置或环境变量设置基准 URL。CI 环境使用的端口可能与本地开发不同。
  4. 首次在 CI 中安装时,不要省略 --with-deps。 Playwright 浏览器在 Linux 上需要系统库(libgbm、libasound 等)。--with-deps 标志会安装这些库。之后使用缓存的运行只需 install-deps。
  5. 不要将 trace 文件存储在代码库中。 Trace 是大型二进制文件。请将其写入已在 git 中忽略的 test-results/ 目录,并作为 CI 工件上传。
  6. 不要为每项测试都创建新的浏览器实例。 启动浏览器的开销很大。使用 IClassFixture 或 Playwright xUnit 基类,在一个类的各项测试之间共享浏览器。每项测试都创建新的 BrowserContext,以实现隔离。

参考资料

评论

支持 Markdown